wealthi-coach-mcp-server
OfficialRead-only access to Firestore collections including user progress, quiz attempts, achievements, and curriculum progress.
Read-only access to Supabase profiles table for student identity, grade band, and coach seen-content tracking.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@wealthi-coach-mcp-server get student progress for uid abc123"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
wealthi-coach-mcp-server
Read-only MCP server giving Wealthi's AI Coach scoped access to student progress data, without the AI ever touching Firestore or Supabase directly.
Why this exists
Before this server, two clients wrote and read student data directly via SDK calls: the dashboard against Supabase, the mobile app against Firestore. Any AI feature built on top of that would need direct database credentials — which violates Wealthi's own AI philosophy ("AI should never receive unrestricted database access," "AI should not expose internal system data"). This server is the single, narrow, auditable path by which AI systems read student data. It does not replace the app's existing read/write paths for UI rendering; it exists specifically for AI consumption.
Related MCP server: OfficeRnD MCP Server
Domain ownership
Domain | System of record | Read by |
XP, streak, points, level | Firestore ( |
|
Quiz attempts | Firestore ( |
|
Achievements | Firestore ( | (reserved — not yet exposed; see Future Work) |
Identity, grade band, parent link | Supabase |
|
Coach seen-content tracker | Supabase |
|
This split is intentional and permanent, not a migration waypoint. See project history for the full reasoning — short version: Firestore owns event-heavy gamification data because that's already its strength and mobile's home turf; Supabase owns identity/relational data because it needs joins and row-level security that Firestore doesn't offer.
Known infrastructure note: as of 2026-06, qsawrfybwwpgajefndnk is the
confirmed-correct Supabase project — verified directly against
wealthihome/.env's VITE_SUPABASE_URL, which is what the live app
actually uses. Wealthi has had multiple Supabase projects connected to the
same Lovable workspace with no "active" indicator in Lovable's panel
itself, so don't trust Lovable's panel alone to determine which project is
correct — always cross-check against the app's actual env var.
Read-only scope (by design, not just by convention)
This server never writes to Firestore or Supabase. All six tools carry
readOnlyHint: true / destructiveHint: false annotations, and the
service layer (src/services/) contains no write methods at all — there's
no update, set, or delete call anywhere in this codebase. Writes
continue to go through existing paths:
Mobile's direct Firestore SDK writes (XP, streak, quiz submission)
Dashboard's existing Supabase writes (profile updates, routing_signals)
The dormancy-decay Edge Function (Supabase) for pattern-state decay
If a future use case needs the AI to act (e.g., "mark this content as seen" instead of just reading seen-content), that should be a deliberately designed, narrowly-scoped write tool added later with its own review — not an extension of this server's existing read tools, and not a broadening of its current credentials to writable ones.
Authentication & Credential Scoping
This server uses two separate, dedicated credentials, neither reused from any other Wealthi service. This matters because of a prior credential exposure incident — the fix isn't just rotating one key, it's making sure no future leak from this server compromises anything beyond what this server itself can read.
Firebase Admin (Firestore access)
In the Firebase console for the project backing the mobile app, go to Project Settings → Service Accounts.
Create a new service account specifically for this server — do not reuse the mobile app's or NestJS API's existing service account.
Grant it the Cloud Datastore Viewer IAM role (Google Cloud Console → IAM, not the Firebase console) — this is a read-only role scoped to Firestore/Datastore. Firestore has no separate "read-only Admin SDK mode"; the restriction must be enforced at the IAM role level. Do not grant
roles/datastore.userorroles/owner— both include write access.Generate a private key (JSON) for this service account and populate
FIREBASE_PROJECT_ID,FIREBASE_CLIENT_EMAIL,FIREBASE_PRIVATE_KEYin.envfrom its contents.Store the JSON file itself in a secrets manager (not in this repo, not in plain
.envin any deployed environment) — inject the three env vars at deploy time instead.
Supabase (identity + routing_signals access)
In the Supabase dashboard for project
qsawrfybwwpgajefndnk, go to Authentication → Policies and confirmprofileshas row-level security enabled (it should already, given existing app usage).Create a dedicated Postgres role or use Supabase's API key management to issue a key scoped to read-only
SELECTonprofilesonly — not theservice_rolekey used by the existing Edge Functions. If Supabase's built-in key types don't offer this granularity directly, create a custom Postgres role withGRANT SELECT ON profiles TO wealthi_coach_readonly;and authenticate via that role's credentials, rather than defaulting toservice_role.Populate
SUPABASE_URLandSUPABASE_COACH_READONLY_KEYin.env.
Rotation discipline
Given the prior exposure incident: rotate both credentials on a fixed schedule (recommend quarterly), and immediately if this repo's CI/CD config, deployment logs, or any environment dump is ever suspected compromised. Because credentials are scoped narrowly and not shared with any other service, rotating them only requires redeploying this server — no coordination needed with the mobile app, dashboard, or NestJS API.
Repo location
This server lives in its own repo (wealthi-coach-mcp-server), separate
from wealthi-ai/wealthihome (dashboard) and the mobile app's repo. It is
not the "Wealthi Intelligence" Supabase project (a separate, unrelated
leads-generation database) — that naming collision was identified and
deliberately avoided when naming this repo. If "Wealthi Intelligence" or
"Wealthi System" comes up in conversation, confirm which of (a) this repo,
(b) the leads-gen Supabase project, or (c) the qsawrfybwwpgajefndnk
Supabase project is actually meant — they are three different things that
have been confused before.
Project structure
wealthi-coach-mcp-server/
├── package.json
├── tsconfig.json
├── .env.example
├── src/
│ ├── index.ts # entry point, transport selection
│ ├── types.ts # Coach-facing domain shapes
│ ├── constants.ts # collection/table names, limits
│ ├── tools/ # MCP tool contracts (thin)
│ │ ├── getStudentProfile.ts
│ │ ├── getStudentProgress.ts
│ │ ├── getAssessmentResults.ts
│ │ ├── getCurriculumProgress.ts
│ │ ├── getLearningSignals.ts
│ │ └── getCoachContext.ts
│ ├── services/ # DB clients + query/composition logic
│ │ ├── firebaseClient.ts
│ │ ├── supabaseClient.ts
│ │ ├── firestoreProgressService.ts
│ │ ├── supabaseProfileService.ts
│ │ ├── curriculumService.ts
│ │ └── learningSignalsService.ts
│ └── schemas/
│ └── studentInput.ts # shared Zod input schemas
└── dist/ # build output (gitignored)Setup
npm install
cp .env.example .env # fill in real credentials, see above
npm run build
npm start # stdio mode by defaultFor HTTP mode (remote deployment):
TRANSPORT=http PORT=3000 npm startTest with the MCP Inspector:
npm run inspectHow the AI Coach client calls this server
The AI Coach currently runs as a feature inside the dashboard
(AICoachCard, coachContent.ts). To call this server from that context,
add it as an MCP server in the Claude API request the Coach feature
already makes:
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-6",
max_tokens: 1000,
messages: [
{
role: "user",
content: "Open a Coach session for this student and recommend what to show next."
}
],
mcp_servers: [
{
type: "url",
url: "https://<your-deployed-host>/mcp",
name: "wealthi-coach-mcp-server"
}
]
})
});In practice, the Coach prompt should instruct the model to call
get_coach_context first, with the current student's ID, before
generating any response — that's the single round-trip that gives it
profile, progress, learning signals, and curriculum state together. The
finer-grained tools (get_student_profile, get_student_progress, etc.)
exist for cases where only one piece is needed, or for debugging which
specific data source is returning unexpected values.
The student ID passed to these tools should come from the authenticated session on the dashboard/mobile side, never from anything the AI itself infers or that a user can supply via chat — that's what keeps this a properly scoped, per-student tool rather than an open query surface.
Future work (not built yet)
getAssessmentResults-style achievement tool (Firestoreachievementscollection is in the domain table but not yet exposed as a tool — add when Coach actually needs to reference specific achievements).Teacher/School platform tools, once those surfaces exist — they belong in this same
tools/+services/structure, not a separate server.If
dormancy-decayand other currently-dormant Edge Functions get reactivated (see open infra note: they're pointed at a different Supabase project,jatohkwzfdoxzevnxrfl, than the live app uses, and have 0 invocations to date),get_learning_signals's momentum derivation may want to read their output directly instead of recomputing a simpler heuristic here.
Available Tools
6 toolsget_assessment_resultsGet Assessment ResultsARead-onlyIdempotent
Retrieve a student's recent quiz/assessment attempts, most recent first.
Reads from Firestore (users/{studentId}/quizAttempts). Paginated since this collection grows unbounded over a student's tenure — always check has_more.
Args:
student_id (string): The student's unique identifier.
limit (number): Max results to return, 1-50 (default 20).
cursor (string, optional): Pagination cursor from a previous call's next_cursor.
Returns: JSON object with schema: { "studentId": string, "results": [ { "attemptId": string, "topic": string, "score": number, // 0-100 "completedAt": string, // ISO 8601 "attemptNumber": number } ], "hasMore": boolean, "nextCursor": string // present only if hasMore is true }
Examples:
Use when: "How did this student do on their last few quizzes?" -> default limit
Use when: "Show me everything" -> paginate using next_cursor until has_more is false
Error Handling:
Returns an empty results array (not an error) if the student has no attempts yet.
Inputs: student_id (required), limit, cursor.
get_coach_contextGet Coach ContextARead-onlyIdempotent
Retrieve everything Coach needs to open a session with a student, in one call.
This is the primary entry point for AI Coach sessions — composes profile, progress, learning signals, and curriculum progress into a single object, so the Coach client doesn't need to know that two separate databases (Firestore and Supabase) are involved. Use the individual tools (get_student_profile, get_student_progress, get_learning_signals, get_curriculum_progress) only when you need just one piece, or for debugging a specific data source.
Args:
student_id (string): The student's unique identifier.
Returns: JSON object with schema: { "profile": { studentId, displayName, gradeBand, hasLinkedParent, enrolledAt }, "progress": { studentId, xp, level, streakCount, streakStatus, pointsBalance }, "signals": { studentId, momentumState, seenContentIds, daysSinceLastSession, recommendedInteractionType }, "curriculum": { studentId, topics: [...] } }
Examples:
Use when: "Start a Coach session for this student" -> call this first, every time, before generating any Coach response
Don't use when: you only need one field (e.g. just streak count) and want to minimize Firestore/Supabase reads -> use the specific tool instead
Error Handling:
Returns "Student not found" if get_student_profile finds no record. Progress/signals/curriculum default to safe empty states for new students who have a profile but haven't started any activity yet.
Inputs: student_id (required).
get_curriculum_progressGet Curriculum ProgressARead-onlyIdempotent
Retrieve a student's completion status across Wealthi's financial literacy topics (saving, budgeting, tradeoffs, delayed gratification, goal setting, decision making, financial confidence, future thinking, credit basics).
Derives completion from passing quiz attempts (score >= 70) grouped by topic and module, mapped against the same topic taxonomy used by Coach's content library (coachContent.ts) — so results align with what's shown in-app.
Args:
student_id (string): The student's unique identifier.
Returns: JSON object with schema: { "studentId": string, "topics": [ { "topicId": string, "topicLabel": string, "status": "not_started" | "in_progress" | "completed", "completedModules": number, "totalModules": number } ] }
Error Handling:
Always returns all 9 topics, even if the student hasn't started any (status will be "not_started" with completedModules: 0).
Inputs: student_id (required).
get_learning_signalsGet Learning SignalsARead-onlyIdempotent
Retrieve behavioral signals that inform what Coach should show a student next.
Composes data from two sources: Supabase (profiles.routing_signals, for already-seen content) and Firestore (last activity timestamp, for momentum). Returns translated behavioral labels only — never raw dormancy-decay scores or internal routing fields. This mirrors the translation Coach already does internally via behaviorLabels.ts.
Args:
student_id (string): The student's unique identifier.
Returns: JSON object with schema: { "studentId": string, "momentumState": "building" | "steady" | "dormant" | "returning", "seenContentIds": string[], // content already shown, for dedup "daysSinceLastSession": number, "recommendedInteractionType": "read" | "decide" | "reflect" }
Examples:
Use when: "What should Coach show this student right now?" -> check recommendedInteractionType and filter coachContent.ts by seenContentIds
Use when: deciding whether to show comeback/re-engagement copy -> check momentumState === "returning"
Error Handling:
A student with no activity history returns momentumState: "dormant" and daysSinceLastSession: 999 (sentinel for "never active"), not an error.
Inputs: student_id (required).
get_student_profileGet Student ProfileARead-onlyIdempotent
Retrieve a student's basic identity and enrollment information.
This tool does NOT return parent contact details, raw assessment data, or any internal scoring/routing fields — only what's needed to personalize Coach's tone and respect age-appropriate framing.
Args:
student_id (string): The student's unique identifier.
Returns: JSON object with schema: { "studentId": string, "displayName": string, // safe display name only "gradeBand": "elementary" | "middle" | "high" | "young_adult", "hasLinkedParent": boolean, // presence only, never parent identity "enrolledAt": string // ISO 8601 date }
Error Handling:
Returns "Student not found" if no profile exists for the given student_id.
Inputs: student_id (required).
get_student_progressGet Student ProgressARead-onlyIdempotent
Retrieve a student's current XP, level, streak, and points balance.
Reads from Firestore (users/{studentId}), which is the system of record for gamification/progress data. Does NOT include quiz-level detail — use get_assessment_results for that.
Args:
student_id (string): The student's unique identifier.
Returns: JSON object with schema: { "studentId": string, "xp": number, "level": number, "streakCount": number, "streakStatus": "active" | "at_risk" | "broken", "pointsBalance": number }
Error Handling:
Returns "Student not found" if no Firestore user document exists for student_id.
Inputs: student_id (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool serves a distinct function: assessment results, composite coach context, curriculum progress, learning signals, student profile, and student progress. No overlap in purpose; descriptions clearly differentiate them.
All tools follow the 'get_' prefix with descriptive noun phrases (e.g., get_student_profile, get_learning_signals). The naming pattern is perfectly consistent.
Six tools cover the essential data retrieval needs for an AI coach session: profile, progress, curriculum, learning signals, assessment results, and a composite tool. This is a well-scoped set that doesn't feel too few or too many.
The tools provide comprehensive coverage of student data: identity, progress metrics, curriculum status, behavioral signals, and quiz results. The composite tool (get_coach_context) further enhances completeness by bundling the most commonly needed data in one call. No obvious gaps for the stated coaching purpose.
Related MCP Connectors
Read-only MCP server for ClassQuill, a tutoring-business-management platform.
Read-only MCP for AI usage profiles, leaderboards, stats, and docs; no writes or private data.
Read-only Google Analytics 4 MCP server for AI assistants.
Read-only MCP server for AIStatusDashboard status, incidents, metrics, and fallback recommendations.
Related MCP Servers
- FlicenseAqualityCmaintenanceRead-only MCP server that enables AI tools like Claude and Cursor to query the Avanti Fellows PostgreSQL database, providing schema inspection, data sampling, and SELECT query execution capabilities.61-
- AlicenseAqualityDmaintenanceA read-only MCP server that connects AI assistants to the OfficeRnD coworking and flex-space management platform. It enables natural language queries for community members, space bookings, billing records, and office resources.51MIT
- AlicenseAqualityDmaintenanceRead-only MCP server that connects AI clients to Crescender's school asset, loan, member, and asset-comms API.6MIT
- AlicenseNot gradedqualityDmaintenancePublic read-only MCP server for FoxTrove Voice, enabling LLMs to query call logs, customer records, assistant stats, and analytics via secure OAuth.MIT