Private ChatGPT app for local SEO and ads management - Built for ZanyCuts in Newnan, GA
A production-ready MCP (Model Context Protocol) server that enables non-technical staff to execute local SEO tasks inside ChatGPT. Generate Wix-ready copy, manage Google Business Profile posts and reviews, monitor Core Web Vitals, and track performance—all from one conversational interface.
- Content and GBP Post - Generate SEO-optimized page copy (title tags, meta descriptions, H1s, intros, alt text, internal links) and publish matching Google Business Profile posts
- Reviews and Reputation - Fetch GBP reviews and generate three human-sounding reply drafts per review
- Speed and Health - Analyze Core Web Vitals (LCP, CLS, INP) with actionable PageSpeed recommendations
- GA4 Event Tracking - Server-side event tracking via Measurement Protocol
- Local Intent Detection - Automatically highlights Newnan, GA-focused queries
- UTM Consistency - Pre-configured UTM parameters for accurate GA4 attribution
- Family-Friendly Tone - All generated content maintains ZanyCuts' welcoming brand voice
- Runtime: Node.js 18+
- Language: TypeScript (strict mode)
- Framework: Express
- APIs: Google Search Console, Business Profile, PageSpeed Insights, GA4 Measurement Protocol
- Authentication: OAuth 2.0 with refresh tokens
- Node.js 18 or higher
- A Google Cloud Project with required APIs enabled
- Google OAuth credentials
- Access to ZanyCuts' Google properties (Search Console, Business Profile, Analytics)
# Clone or navigate to the project directory
cd /home/clint/projects/SEODesk
# Install dependencies
npm install
# Copy environment template
cp env.example .env
# Edit .env and fill in all values (see OAuth Setup below)
nano .env
# Build the TypeScript project
npm run build
# Start the development server
npm run devThe server will start on http://localhost:3000 by default.
# Health check
curl http://localhost:3000/health
# Expected response:
# {"status":"healthy","version":"1.0.0","timestamp":"2025-10-09T..."}- Go to Google Cloud Console
- Create a new project or select an existing one
- Note your Project ID
Enable these APIs in your Google Cloud Project:
- Google Search Console API
- Google My Business API (Business Profile)
- PageSpeed Insights API
- Google Analytics Data API
# Or enable via gcloud CLI:
gcloud services enable searchconsole.googleapis.com
gcloud services enable mybusiness.googleapis.com
gcloud services enable pagespeedonline.googleapis.com
gcloud services enable analyticsdata.googleapis.com- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Choose Web application
- Add authorized redirect URIs:
http://localhost:3000/oauth/callback(for local development)- Your production URL (if deploying to Cloud Run)
- Click Create
- Copy the Client ID and Client Secret
- Add them to your
.envfile:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secretYou need to perform a one-time OAuth flow to get a refresh token:
- Install OAuth Playground Helper (or use Google's OAuth Playground):
# Using Google OAuth Playground (easiest method)
# 1. Go to https://developers.google.com/oauthplayground/
# 2. Click the gear icon (⚙️) in the top right
# 3. Check "Use your own OAuth credentials"
# 4. Enter your Client ID and Client Secret
# 5. In Step 1, select these scopes:
# - https://www.googleapis.com/auth/webmasters.readonly
# - https://www.googleapis.com/auth/business.manage
# 6. Click "Authorize APIs"
# 7. Sign in with the Google account that has access to ZanyCuts properties
# 8. In Step 2, click "Exchange authorization code for tokens"
# 9. Copy the "Refresh token" value- Add the refresh token to
.env:
GOOGLE_REFRESH_TOKEN=1//your-refresh-token-hereFormat: sc-domain:yourdomain.com or https://www.yourdomain.com/
# Find your property URL:
# 1. Go to https://search.google.com/search-console
# 2. Select ZanyCuts property
# 3. Copy the URL from the browser or use the exact property name
GSC_PROPERTY_URL=sc-domain:zanycuts.comFormat: accounts/{account_id}/locations/{location_id}
# Find your location name:
# 1. Go to https://business.google.com/
# 2. Select ZanyCuts location
# 3. Look at the URL: business.google.com/u/0/locations/{location_id}
# 4. For account_id, you may need to use the GBP API or contact support
# Or use this helper script:
# Option: Use the GBP API to list locations (after setting up OAuth)
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" \
https://mybusinessbusinessinformation.googleapis.com/v1/accounts/YOUR_ACCOUNT_ID/locations
GBP_LOCATION_NAME=accounts/123456789/locations/987654321# Find Measurement ID:
# 1. Go to GA4 Admin > Data Streams
# 2. Select your web stream
# 3. Copy the Measurement ID (format: G-XXXXXXXXXX)
GA4_MEASUREMENT_ID=G-XXXXXXXXXX
# Create API Secret:
# 1. In the same Data Stream settings, scroll to "Measurement Protocol API secrets"
# 2. Click "Create"
# 3. Give it a nickname (e.g., "SEO Desk Server")
# 4. Copy the secret value
GA4_API_SECRET=your-measurement-protocol-secret# Start the server
npm run dev
# Test Search Console access
curl http://localhost:3000/get_gsc_queries
# If successful, you'll see query data
# If you see an error, check your OAuth credentials and scopesFetches Search Console queries for the last 28 days with local intent highlighting.
Example:
curl http://localhost:3000/get_gsc_queriesResponse:
{
"queries": [
{
"query": "kids haircuts newnan ga",
"page": "https://zanycuts.com/kids-haircuts",
"clicks": 45,
"impressions": 890,
"ctr": 0.051,
"position": 3.2,
"localIntent": true
}
],
"dateRange": {
"start": "2025-09-11",
"end": "2025-10-09"
}
}Generates SEO-optimized page copy for Wix.
Example:
curl -X POST http://localhost:3000/compose_page_copy \
-H "Content-Type: application/json" \
-d '{
"query": "kids haircuts newnan ga",
"targetUrl": "https://zanycuts.com/kids-haircuts"
}'Response:
{
"copy": {
"titleTag": "Kids Haircuts in Newnan, GA | ZanyCuts",
"metaDescription": "Family-friendly kids haircuts in Newnan, GA with patient stylists and easy online booking...",
"h1": "Kids Haircuts in Newnan, GA",
"introParagraph": "Looking for kids haircuts in Newnan, GA? ZanyCuts specializes...",
"imageAltText": "Child enjoying kids haircuts at ZanyCuts in Newnan GA",
"internalLinks": [
{ "text": "Kids' Cuts", "url": "/kids-cuts" },
{ "text": "First Haircut Package", "url": "/first-haircut" }
],
"gbpDirectionsUrl": "https://g.page/zanycuts?utm_source=gbp&utm_medium=organic&utm_campaign=local"
},
"gbpPostDraft": "New page alert! 🎉 We just updated our Kids Haircuts page..."
}Publishes a post to Google Business Profile.
Example:
curl -X POST http://localhost:3000/write_gbp_post \
-H "Content-Type: application/json" \
-d '{
"message": "New page alert! Check out our updated Kids Haircuts page with all the info about our gentle stylists in Newnan, GA!",
"ctaUrl": "https://zanycuts.com/kids-haircuts?utm_source=gbp&utm_medium=organic&utm_campaign=local",
"ctaType": "LEARN_MORE"
}'Response:
{
"success": true,
"postName": "locations/123456789/localPosts/987654321",
"publishTime": "2025-10-09T14:30:00Z"
}Fetches latest Google Business Profile reviews with suggested reply drafts.
Example:
curl http://localhost:3000/fetch_reviews?limit=5Response:
{
"reviews": [
{
"reviewId": "review_123",
"starRating": 5,
"comment": "My daughter loved her first haircut here!",
"reviewer": "Sarah M.",
"createTime": "2025-10-05T10:20:00Z",
"hasReply": false,
"suggestedReplies": [
{
"version": 1,
"text": "Thank you so much, Sarah! We're thrilled your daughter had such a positive first haircut experience..."
}
]
}
]
}Posts an owner reply to a Google Business Profile review.
Example:
curl -X POST http://localhost:3000/reply_to_review \
-H "Content-Type: application/json" \
-d '{
"reviewId": "review_123",
"reply": "Thank you so much, Sarah! We'\''re thrilled your daughter had such a positive experience with us."
}'Response:
{
"success": true,
"replyTime": "2025-10-09T14:35:00Z"
}Analyzes Core Web Vitals using PageSpeed Insights.
Example:
curl -X POST http://localhost:3000/check_pagespeed \
-H "Content-Type: application/json" \
-d '{
"url": "https://zanycuts.com/kids-haircuts",
"strategy": "mobile"
}'Response:
{
"url": "https://zanycuts.com/kids-haircuts",
"strategy": "mobile",
"metrics": {
"lcp": { "value": 2.3, "score": 0.92, "status": "PASS" },
"cls": { "value": 0.05, "score": 0.98, "status": "PASS" },
"inp": { "value": 150, "score": 0.85, "status": "PASS" }
},
"recommendations": [
"Compress hero image (current size: 2.4MB)",
"Defer non-critical JavaScript"
],
"issuesSummary": "All Core Web Vitals metrics passing. Consider image compression for faster load times."
}Sends a server-side event to GA4 via Measurement Protocol.
Example:
curl -X POST http://localhost:3000/send_ga4_event \
-H "Content-Type: application/json" \
-d '{
"eventName": "content_published",
"params": {
"page_type": "service_page",
"keyword": "kids haircuts newnan ga"
}
}'Response:
{
"success": true,
"clientId": "gen_1234567890.1234567890"
}Local SEO helps ZanyCuts appear in search results when families in Newnan, GA search for kids' haircuts and related services. Here's what you need to know:
Rule: Every service needs its own page with "Newnan, GA" targeting.
Why: Google understands specific pages better than generic "Services" pages.
Required Elements Per Page:
- Title Tag: Must include service + "Newnan, GA" + "ZanyCuts"
- Example:
Kids Haircuts in Newnan, GA | ZanyCuts
- Example:
- Meta Description: Brief description with location and service
- H1: Service name + "Newnan, GA"
- First Paragraph: Must mention Newnan, GA and include booking CTA
- Image Alt Text: Service + "Newnan GA" (natural, not stuffed)
What It Is: Links between your own pages (e.g., Kids' Cuts → First Haircut Package)
Why It Helps:
- Helps Google understand relationships between services
- Keeps visitors on your site longer
- Distributes "page authority" across your site
Best Practice: Link to 5-7 related pages in each service page's content.
Post Freshness:
- Post at least 1-2 times per week
- Fresh posts signal to Google that your business is active
- Use photos when possible (kids getting haircuts, fun moments)
Review Velocity:
- Encourage happy customers to leave reviews
- More recent reviews = higher local rankings
- Respond to ALL reviews (good and bad) within 24-48 hours
Why GBP Posts Matter:
- They appear in local search results
- They provide fresh content signals to Google
- They drive traffic to specific pages with UTM tracking
What They Are: Tags added to URLs that tell GA4 where traffic came from.
Standard UTM Structure for ZanyCuts:
?utm_source=gbp&utm_medium=organic&utm_campaign=local
Why Consistency Matters:
- GA4 groups traffic by UTM parameters
- Inconsistent UTMs = fragmented data
- Use the EXACT same UTMs across all GBP posts and links
UTM Breakdown:
utm_source=gbp- Traffic from Google Business Profileutm_medium=organic- Unpaid (organic) trafficutm_campaign=local- Part of your local SEO campaign
What They Are: Google's official page speed metrics that affect rankings.
The Big Three:
- LCP (Largest Contentful Paint) - How fast the main content loads
- Goal: Under 2.5 seconds
- CLS (Cumulative Layout Shift) - How much the page "jumps" while loading
- Goal: Under 0.1
- INP (Interaction to Next Paint) - How quickly the page responds to clicks
- Goal: Under 200ms
Quick Wins:
- Compress large images (especially hero images)
- Defer non-critical JavaScript
- Use web fonts efficiently
- Google Cloud SDK installed (
gcloud) - Docker installed (optional, Cloud Run can build from source)
- A Google Cloud Project with billing enabled
- Authenticate with Google Cloud:
gcloud auth login
gcloud config set project YOUR_PROJECT_ID- Build and Deploy:
# From the project root
gcloud run deploy zanycutsdesk \
--source . \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars NODE_ENV=production- Set Environment Variables:
# Set all required environment variables
gcloud run services update zanycutsdesk \
--region us-central1 \
--update-env-vars \
GOOGLE_CLIENT_ID=your-client-id,\
GOOGLE_CLIENT_SECRET=your-client-secret,\
GOOGLE_REFRESH_TOKEN=your-refresh-token,\
GSC_PROPERTY_URL=sc-domain:zanycuts.com,\
GBP_LOCATION_NAME=accounts/123/locations/456,\
GA4_MEASUREMENT_ID=G-XXXXXXXXXX,\
GA4_API_SECRET=your-api-secret- Get Service URL:
gcloud run services describe zanycutsdesk --region us-central1 --format 'value(status.url)'- Test Deployment:
curl https://zanycutsdesk-xxxxx-uc.a.run.app/health- All environment variables set in Cloud Run
- OAuth credentials verified (test
/get_gsc_queries) - Search Console access working
- GBP post publishing working (test with draft post)
- GA4 events appearing in DebugView
- PageSpeed API responding
- Review fetch working
- All endpoints returning valid responses
- HTTPS certificate valid (automatic with Cloud Run)
- Logs showing no errors in Cloud Logging
Error: OAUTH_REFRESH_FAILED
Solution:
- Verify
GOOGLE_REFRESH_TOKENin.env - Check that refresh token hasn't been revoked (test in OAuth Playground)
- Ensure OAuth client credentials match (Client ID and Secret)
Error: GSC_UNAUTHORIZED
Solution:
- Verify the Google account used for OAuth has Search Console access
- Check that
GSC_PROPERTY_URLexactly matches the property in Search Console - Ensure the OAuth scope
https://www.googleapis.com/auth/webmasters.readonlywas included
Error: GBP_LOCATION_NOT_FOUND
Solution:
- Verify
GBP_LOCATION_NAMEformat:accounts/{account_id}/locations/{location_id} - Test location access via GBP API or Business Profile Manager
- Ensure the OAuth account has management access to the location
Error: RATE_LIMIT_EXCEEDED
Solution:
- Wait 60 seconds and retry
- Implement exponential backoff for automated requests
- Consider caching GSC data for frequently accessed queries
Issue: Events sent but not showing in GA4 DebugView
Solution:
- Verify
GA4_MEASUREMENT_IDandGA4_API_SECRET - Use the
/send_ga4_eventendpoint with a test event - Check GA4 DebugView within 5-10 minutes (events can be delayed)
- Ensure event names follow GA4 naming rules (alphanumeric and underscores only)
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch# Run ESLint
npm run lint
# Format code with Prettier
npm run formatsrc/
├── config/ # Environment configuration
├── oauth/ # OAuth token management
├── clients/ # Google API clients
├── services/ # Business logic (content, replies)
├── routes/ # Express route handlers
├── types/ # TypeScript type definitions
└── index.ts # Server entry point
For questions or issues, contact the ZanyCuts development team or refer to:
- Google Search Console API Docs
- Google Business Profile API Docs
- PageSpeed Insights API Docs
- GA4 Measurement Protocol Docs
Private - ZanyCuts Internal Use Only