MCP server
Connect Claude Code, Claude Desktop, Cursor, or any MCP-aware client to a Project88 workspace for read-only access to your data tables, calendar, dial, inbox, pages, and templates — thirty tools, all scoped to a single user's own permissions.
Project88 exposes a hosted Model Context Protocol server so MCP-aware clients — Claude Code, Claude Desktop, Cursor, and others — can read from your org directly. No browser session, no copy-paste of data: the client calls tools against your org, and your permissions follow the token.
The server runs as the mcp Supabase Edge Function, speaks JSON-RPC 2.0
over Streamable HTTP (MCP 2025-06-18), and authenticates via per-user
bearer tokens (p88_…).
What it exposes
Thirty read-only tools across the six native apps — Tables, Calendar, Dial, Inbox, Pages, and Templates — plus two system tools.
| App | Tools |
|---|---|
| System | whoami, get_current_time |
| Tables | list_tables, get_table, query_table_rows, get_table_row, list_table_views, list_row_activities |
| Calendar | list_calendars, list_calendar_events, get_calendar_event, search_calendar_events |
| Dial | list_dial_buckets, get_dial_bucket, get_dial_bucket_stats, get_dial_buckets_overview, list_dial_bucket_calls, list_dial_suppressions |
| Inbox | list_email_accounts, list_inbox, get_email_thread, get_email_threads, list_email_labels |
| Pages | list_pages, search_pages, search_page_content, get_page |
| Templates | list_templates, get_template, search_templates |
Notes:
- Row values on tables live in a
dataobject keyed by column id. Calllist_tablesorget_tablefirst to interpret them. - Calendar collapses duplicate mirror copies of the same meeting and reports how many were merged.
- Dial calls the same
dial_bucket_stats/dial_buckets_overviewRPCs the app uses, so counts (including meetings) match what a rep sees in the shell. - Inbox reads Gmail or Microsoft Graph through your own OAuth
connection; prefer the batch
get_email_threadsover N calls toget_email_thread. - Pages are serialized to markdown; if a block type isn't
recognised, the response reports it in
unrecognized_block_typesrather than dropping content silently. - Templates preserve merge tags (
{{first_name}}) verbatim — they're content, not variables.
No write tools. No send, create, update, or delete surface exists.
MCP clients call tools without confirming each one, so a hallucinated
delete_event shouldn't be able to land in production data. A
confirming write-tool surface is on the roadmap.
Connect Claude Code
-
Mint a token. In Project88: Settings → MCP access → Create a token. The raw value is shown exactly once — copy it now. Only a sha-256 hash is stored.
-
Export it in the shell you'll launch Claude Code from:
export PROJECT88_MCP_TOKEN='p88_...' -
Add a
.mcp.jsonat the root of your project (Claude Code auto-loads it):{ "mcpServers": { "project88": { "type": "http", "url": "https://<your-project-ref>.supabase.co/functions/v1/mcp", "headers": { "Authorization": "Bearer ${PROJECT88_MCP_TOKEN}" } } } } -
Verify. Start a new session and ask "who am I in Project88?" — Claude should call
whoamiand name your user and org. Or check the connection listing:claude mcp list
Connect Claude Desktop, Cursor, or another MCP client
Any client that speaks the same JSON-RPC-over-HTTP transport works the
same way. Point it at the endpoint from step 3 above and send the token
as Authorization: Bearer p88_….
What a token can and cannot do
A token authenticates as one user in one organization. Every query runs through that user's own Row Level Security policies, so:
- It reads exactly what its owner can read in the app — no more.
- It cannot read another organization's data, even by accident — isolation is enforced by Postgres, not by application code remembering to filter.
- It stops working the moment its owner leaves the org, or the token is revoked.
- It is read-only.
The token grants your permissions to whoever holds it. Treat it like a password: keep it in an environment variable, never commit it.
Revoking
Settings → MCP access → Revoke token. Revocation is immediate — the
server also drops its cached session for the token, so an in-flight
client stops working at once. Tokens are revoked rather than deleted,
so created_at and last_used_at survive for audit.
Conventions
Pagination. List tools take limit / offset and return
pagination.has_more. An exact total is omitted unless you pass
include_total: true — computing one is a full scan under the row's
RLS predicate.
Errors. Tool failures come back as a normal result with
isError: true and a plain-language message, so a model can recover.
Database internals are never included; an unrecognised failure returns
a generic message plus a short reference id that matches a server log
line.
Ids. Tools expect table_id, event_id, bucket_id and friends
as UUIDs. Get them from the corresponding list_* tool first — they
aren't guessable.
How auth works (under the hood)
Each request carries Authorization: Bearer p88_…. The mcp function:
- sha-256-hashes the token and looks it up in
mcp_tokens; - exchanges it for a real user Supabase session for the token's
owner (cached in-memory and in
mcp_sessionswith the live session held in Vault, so cold starts don't pay two round trips); - runs the tool's queries through that session — so RLS is the
enforcement boundary, not
.eq('org_id', …)filters written by hand in each tool.
Unknown, revoked, or non-p88_-prefixed bearer values are rejected
with 401 Unauthorized.
The user-session model is what makes Inbox work at all: mailbox
credentials in integrations are keyed by user_id, not org_id, so
an org-only identity has nothing to resolve against.
Limits and caveats
- One user + one org per token. If you administer multiple orgs, or want to expose a different member's view, mint a separate token per scope and add each as its own MCP server entry.
- Read-only by design.
- Rate limiting. None yet at the MCP layer; upstream providers (Gmail, Graph) enforce their own. Per-token quotas will land before this surface is opened to end-customers.
- Inbox needs a connected mailbox. The user the token belongs to must have Gmail or Microsoft connected under Integrations; an unconnected user gets an explicit error, not empty results.
Troubleshooting
| Symptom | Likely cause |
|---|---|
401 Unauthorized | Token missing, malformed, revoked, or PROJECT88_MCP_TOKEN not exported in the shell that launched the client. |
405 on GET | Expected — the server has no server→client stream; clients fall back to POST. |
| Tools listed but every call fails | The token's owner may have been removed from the org. |
| Inbox tools say no account connected | That user has no mailbox connected — integrations are per-user, not per-org. |
| Inbox says several mailboxes are connected | Pass integration_id from list_email_accounts. |
| A tool returns "not available on this server" | Schema drift — a column referenced by a tool has been renamed or dropped. Regenerate the tool's schema and redeploy. |
Where to next
- Tools — how Project88's own agents call tools
- Data tables — the shape behind
query_table_rows - Inbox — the mailbox surface behind the inbox tools
- Vault and secrets — how Project88 stores credentials in general