wealthi-coach-mcp-server
Official# 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.
## Domain ownership
| Domain | System of record | Read by |
|---|---|---|
| XP, streak, points, level | Firestore (`users/{uid}`) | `get_student_progress`, `get_coach_context` |
| Quiz attempts | Firestore (`users/{uid}/quizAttempts`) | `get_assessment_results`, `get_curriculum_progress` |
| Achievements | Firestore (`users/{uid}/achievements`) | *(reserved — not yet exposed; see Future Work)* |
| Identity, grade band, parent link | Supabase `profiles` (project `qsawrfybwwpgajefndnk`) | `get_student_profile`, `get_coach_context` |
| Coach seen-content tracker | Supabase `profiles.routing_signals` | `get_learning_signals`, `get_coach_context` |
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)
1. In the Firebase console for the project backing the mobile app, go to
**Project Settings → Service Accounts**.
2. Create a **new** service account specifically for this server — do not
reuse the mobile app's or NestJS API's existing service account.
3. 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.user` or `roles/owner` — both include write
access.
4. Generate a private key (JSON) for this service account and populate
`FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`
in `.env` from its contents.
5. Store the JSON file itself in a secrets manager (not in this repo, not
in plain `.env` in any deployed environment) — inject the three env
vars at deploy time instead.
### Supabase (identity + routing_signals access)
1. In the Supabase dashboard for project `qsawrfybwwpgajefndnk`, go to
**Authentication → Policies** and confirm `profiles` has row-level
security enabled (it should already, given existing app usage).
2. Create a dedicated Postgres role or use Supabase's API key management
to issue a key scoped to read-only `SELECT` on `profiles` only —
**not** the `service_role` key used by the existing Edge Functions.
If Supabase's built-in key types don't offer this granularity
directly, create a custom Postgres role with `GRANT SELECT ON
profiles TO wealthi_coach_readonly;` and authenticate via that role's
credentials, rather than defaulting to `service_role`.
3. Populate `SUPABASE_URL` and `SUPABASE_COACH_READONLY_KEY` in `.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
```bash
npm install
cp .env.example .env # fill in real credentials, see above
npm run build
npm start # stdio mode by default
```
For HTTP mode (remote deployment):
```bash
TRANSPORT=http PORT=3000 npm start
```
Test with the MCP Inspector:
```bash
npm run inspect
```
## How 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:
```javascript
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 (Firestore `achievements`
collection 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-decay` and 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.
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.