mcp-briven-ask.ts678 lines · main
| 1 | import { newId } from '@briven/shared'; |
| 2 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; |
| 3 | import { eq, sql } from 'drizzle-orm'; |
| 4 | import { z } from 'zod'; |
| 5 | |
| 6 | import { getDb } from '../db/client.js'; |
| 7 | import { mcpKnownAnswers } from '../db/schema.js'; |
| 8 | import { log } from '../lib/logger.js'; |
| 9 | import { |
| 10 | validateStoredAnswer, |
| 11 | writeGroundedAnswer, |
| 12 | type GroundedAnswer, |
| 13 | } from './mcp-answer-writer.js'; |
| 14 | |
| 15 | /** |
| 16 | * `briven_ask` — the general MCP reception desk (Build 3, owner-approved |
| 17 | * 2026-07-07). Extends the auth bridge idea to EVERY briven functional area. |
| 18 | * |
| 19 | * THE ANSWERING CONTRACT (hard rule — a question is NEVER wiped off): |
| 20 | * every answer has three parts, even when the asked-for feature does not |
| 21 | * exist on briven: |
| 22 | * 1. howBrivenWorksHere — the honest platform picture for that area |
| 23 | * 2. whatOurToolsGiveYou — the closest primitives briven has today |
| 24 | * 3. whatYouBuildInYourProject — the remaining gap the ASKING agent |
| 25 | * solves in its own codebase, with a concrete suggestion |
| 26 | * plus a docs citation, so agents learn the official path. |
| 27 | * |
| 28 | * When no curated area matches, the tool still answers (platform overview + |
| 29 | * docs index) AND files the question into the review stream (audit log) so a |
| 30 | * briven session can extend these guides — the desk gets permanently smarter, |
| 31 | * dead ends do not exist. |
| 32 | * |
| 33 | * Read-only. One-project key scope. Vendor names never appear in prose. |
| 34 | */ |
| 35 | |
| 36 | const DOCS_BASE = 'https://docs.briven.tech'; |
| 37 | |
| 38 | /* ── the area guides — hand-curated, three-part contract ────────────── */ |
| 39 | |
| 40 | export interface BrivenAreaGuide { |
| 41 | readonly id: string; |
| 42 | readonly area: string; |
| 43 | readonly keywords: readonly string[]; |
| 44 | readonly howBrivenWorksHere: string; |
| 45 | readonly whatOurToolsGiveYou: readonly string[]; |
| 46 | readonly whatYouBuildInYourProject: readonly string[]; |
| 47 | readonly docs: string; |
| 48 | } |
| 49 | |
| 50 | export const BRIVEN_AREA_GUIDES: readonly BrivenAreaGuide[] = [ |
| 51 | { |
| 52 | id: 'database', |
| 53 | area: 'database / tables / SQL', |
| 54 | keywords: [ |
| 55 | 'database', 'table', 'tables', 'sql', 'query', 'row', 'rows', 'column', |
| 56 | 'schema', 'insert', 'update', 'delete', 'join', 'index', 'postgres', 'doltgres', |
| 57 | ], |
| 58 | howBrivenWorksHere: |
| 59 | 'briven is DOLTGRES-FIRST: every project gets its own isolated Doltgres database ' + |
| 60 | '(Postgres wire protocol + git-for-data: commits, branches, time-travel). the ' + |
| 61 | 'platform control plane also runs on Doltgres. you never get a raw superuser ' + |
| 62 | 'login to another project. define tables via the schema DSL or studio; read/write ' + |
| 63 | 'via SDK, HTTP functions, or this MCP.', |
| 64 | whatOurToolsGiveYou: [ |
| 65 | 'MCP: list_tables, describe_table, query (read); create_table, insert, update, delete (write scope).', |
| 66 | 'MCP lifecycle: db_health + db_recovery_points (read); db_restart + db_recover (write scope); db_reprovision (admin keys) — for "database not responding" start with db_health, then db_restart.', |
| 67 | 'SDK: typed queries + mutations from @briven/client and the framework packages.', |
| 68 | 'versioning: undo/snapshots per docs /undo and /doltgres (SELECT dolt_commit / dolt_log — not MySQL CALL DOLT_*).', |
| 69 | ], |
| 70 | whatYouBuildInYourProject: [ |
| 71 | 'your domain model and validation live in YOUR app — briven stores and versions the data, it does not encode your business rules.', |
| 72 | 'batch big imports through the bulk values array on insert to stay under per-minute rate limits.', |
| 73 | 'do NOT stand up a side Postgres "because you want SQL" — use briven tables + functions. if a query fails, ask briven_ask with the exact error (Doltgres beta has some gaps vs stock Postgres).', |
| 74 | ], |
| 75 | docs: `${DOCS_BASE}/doltgres`, |
| 76 | }, |
| 77 | { |
| 78 | id: 'app-data-access', |
| 79 | area: 'reading & writing your data from a running app (any language / stack)', |
| 80 | keywords: [ |
| 81 | 'python', 'ruby', 'go', 'golang', 'rust', 'java', 'php', 'csharp', 'dotnet', |
| 82 | 'node', 'client', 'sdk', 'driver', 'connection', 'connect', 'dsn', 'psql', |
| 83 | 'libpq', 'orm', 'runtime', 'app', 'application', 'stack', 'language', |
| 84 | 'fetch', 'request', 'curl', 'read', 'write', 'crud', 'data', |
| 85 | ], |
| 86 | howBrivenWorksHere: |
| 87 | 'your running app never opens a raw sql connection to briven, and there is NO ' + |
| 88 | '"run any sql with my key" http endpoint — that is deliberate: the same ' + |
| 89 | 'mediation that keeps every project sealed in its own database. instead an app ' + |
| 90 | 'reads/writes by calling small server FUNCTIONS that run next to the data, while ' + |
| 91 | 'agents and tooling use this MCP\'s data tools. this is identical for every ' + |
| 92 | 'language — the javascript sdk is a convenience wrapper over plain http, not a ' + |
| 93 | 'requirement.', |
| 94 | whatOurToolsGiveYou: [ |
| 95 | 'from ANY language (python, go, ruby, rust, java, …): POST https://api.briven.tech/v1/projects/<projectId>/functions/<functionName> with header "authorization: bearer <brk_ server key>" and a json body — that single http call IS the runtime data path, no sdk required.', |
| 96 | 'the brk_ key must be minted at role developer or higher to invoke functions; a lower-scope key returns 403.', |
| 97 | 'to define or inspect tables while building: this MCP — create_table, describe_table, query, insert, update, delete.', |
| 98 | 'javascript / typescript apps can use @briven/client (invoke + realtime subscribe) instead of hand-writing the fetch.', |
| 99 | ], |
| 100 | whatYouBuildInYourProject: [ |
| 101 | 'write one small function per operation (e.g. saveRun, listRuns) that does the ctx.db work, deploy it with the cli, then call it over http from your app.', |
| 102 | 'there is no first-party sdk for python / go / etc yet — call the http endpoint directly (a one-line request in any http library). a missing convenience wrapper is NOT a reason to stand up a side database or ask for a raw sql login.', |
| 103 | 'keep the brk_ key in server-side env only, never in client / browser code.', |
| 104 | ], |
| 105 | docs: `${DOCS_BASE}/functions`, |
| 106 | }, |
| 107 | { |
| 108 | id: 'storage', |
| 109 | area: 'file storage / uploads / sharing', |
| 110 | keywords: [ |
| 111 | 'storage', 'file', 'files', 'upload', 'download', 'bucket', 's3', 'image', |
| 112 | 'video', 'presigned', 'public', 'share', 'link', 'quota', 'transform', |
| 113 | ], |
| 114 | howBrivenWorksHere: |
| 115 | 'per-project private S3 buckets on briven MinIO (s3.briven.tech): each project ' + |
| 116 | 'gets its own bucket (proj-…) and scoped keys. soft-delete with recovery window, ' + |
| 117 | 'public media URLs on media.briven.tech, image transforms, share-links, tier quotas. ' + |
| 118 | 'this is NOT the platform database backup vault — it is app file storage only.', |
| 119 | whatOurToolsGiveYou: [ |
| 120 | 'MCP: storage_list_files, storage_usage, storage_upload_url, storage_download_url, storage_delete_file, storage_list_deleted, storage_restore_file, storage_make_public, storage_transform_url.', |
| 121 | 'MCP keys: storage_mint_key / storage_list_keys / storage_revoke_key (S3-compatible access for external tools).', |
| 122 | 'MCP sharing: storage_create_link / storage_list_links / storage_revoke_link; grants tools (cross-project read is deny-by-default).', |
| 123 | ], |
| 124 | whatYouBuildInYourProject: [ |
| 125 | 'upload FROM THE BROWSER with the presigned URL — never proxy file bytes through your own server.', |
| 126 | 'store the returned file id in one of your tables to link files to your domain objects.', |
| 127 | 'check storage_usage before large ingests; quota blocks are tier-dependent.', |
| 128 | 'use storage_list_deleted + storage_restore_file (or dashboard Recently deleted) to undo accidental deletes within the recovery window.', |
| 129 | ], |
| 130 | docs: `${DOCS_BASE}/storage`, |
| 131 | }, |
| 132 | { |
| 133 | id: 'functions', |
| 134 | area: 'server functions / backend logic', |
| 135 | keywords: [ |
| 136 | 'function', 'functions', 'runtime', 'server', 'backend', 'mutation', 'action', |
| 137 | 'cron', 'endpoint', 'invoke', 'logic', 'webhook', 'ulid', |
| 138 | ], |
| 139 | howBrivenWorksHere: |
| 140 | 'you write typescript functions (queries, mutations, actions) that run ON briven ' + |
| 141 | 'next to your data — deployed via the CLI, invoked from the SDK or HTTP. the ' + |
| 142 | 'runtime exposes ctx.db, mutation/action helpers, ulid, and brivenError.', |
| 143 | whatOurToolsGiveYou: [ |
| 144 | 'CLI deploy of your functions folder; invoke via SDK or POST /v1/invoke.', |
| 145 | 'runtime helpers: mutation/action/ulid/brivenError (available since 2026-07-06).', |
| 146 | 'fault isolation: one broken function does not take down your other functions.', |
| 147 | ], |
| 148 | whatYouBuildInYourProject: [ |
| 149 | 'keep functions small and data-close; orchestration and UI state belong in your app.', |
| 150 | 'for third-party calls (payment providers, external APIs) use an action, not a mutation.', |
| 151 | 'throw brivenError(code, message, {status}) for clean client-side error handling.', |
| 152 | ], |
| 153 | docs: `${DOCS_BASE}/functions`, |
| 154 | }, |
| 155 | { |
| 156 | id: 'realtime', |
| 157 | area: 'realtime / live queries / subscriptions', |
| 158 | keywords: [ |
| 159 | 'realtime', 'live', 'subscribe', 'subscription', 'websocket', 'reactive', |
| 160 | 'push', 'sync', 'presence', 'update', 'instantly', |
| 161 | ], |
| 162 | howBrivenWorksHere: |
| 163 | 'reactive queries: the SDK subscribes to a query and briven pushes fresh results ' + |
| 164 | 'when underlying rows change — no polling loop on your side.', |
| 165 | whatOurToolsGiveYou: [ |
| 166 | 'SDK: useQuery-style reactive hooks in @briven/react (svelte stores / vue composables likewise).', |
| 167 | 'reconnect + backoff handled by the client; ws token flows from your keys automatically.', |
| 168 | ], |
| 169 | whatYouBuildInYourProject: [ |
| 170 | 'design queries to be narrow (per-view) — a subscription re-runs on relevant writes.', |
| 171 | 'presence/typing-indicator style features: model them as small tables your clients write to; the reactive query does the fan-out.', |
| 172 | ], |
| 173 | docs: `${DOCS_BASE}/realtime`, |
| 174 | }, |
| 175 | { |
| 176 | id: 'auth', |
| 177 | area: 'end-user auth / sign-in / auth emails', |
| 178 | keywords: [ |
| 179 | 'auth', 'login', 'signin', 'signup', 'magic', 'otp', 'passkey', 'oauth', |
| 180 | 'session', 'sender', 'email', 'domain', 'user', 'users', 'password', |
| 181 | 'jwt', 'jwks', 'token', 'verify', 'webauthn', 'face', '500', 'cors', |
| 182 | 'google', 'konnos', |
| 183 | ], |
| 184 | howBrivenWorksHere: |
| 185 | 'managed multi-tenant end-user auth on briven-engine (Doltgres briven_engine): email+password, magic link, ' + |
| 186 | 'email OTP, passkeys (WebAuthn), OAuth, TOTP MFA — per project. Live HTTP surface is /v1/auth-core/* ' + |
| 187 | '(FDI under /v1/auth-core/fdi/*). Retired /v1/auth-tenant/* returns 410 Gone. ' + |
| 188 | 'ALWAYS call auth_config_get first. Magic/OTP/passkey only need enabled:true; OAuth also needs ' + |
| 189 | 'clientIdSet:true + secret. Unverified senderDomain falls back to noreply@briven.tech (sign-in still works). ' + |
| 190 | 'Passwordless: POST /v1/auth-core/fdi/signinup/code + …/code/consume. Session: GET /v1/auth-core/session/me. ' + |
| 191 | 'Passkeys: POST /v1/auth-core/fdi/webauthn/signin/options + …/finish. Prefer first-party proxy for cookies. ' + |
| 192 | 'Browser key: pk_briven_auth_… only (never brk_).', |
| 193 | whatOurToolsGiveYou: [ |
| 194 | 'MCP read: auth_config_get, sender_domain_status, auth_docs_ask (guidance must cite auth-core FDI, not auth-tenant).', |
| 195 | 'MCP write (read-write/admin key): auth_enable_passwordless (magic+OTP+passkey ON + provision), auth_mint_public_key (pk_briven_auth_… once).', |
| 196 | 'HTTP with admin brk_: PATCH /v1/projects/:id/auth/config, POST …/auth/enable, POST …/auth/api-keys, POST …/auth/allowed-domains (project keys work — not session-only).', |
| 197 | 'CLI: briven setup (new) OR briven connect p_… then briven auth scaffold — never setup --project.', |
| 198 | 'Sign-in paths: POST /v1/auth-core/fdi/signinup/code, POST …/signinup/code/consume, GET /v1/auth-core/session/me, POST …/webauthn/*.', |
| 199 | ], |
| 200 | whatYouBuildInYourProject: [ |
| 201 | 'if providers OFF: call auth_enable_passwordless (write MCP) or PATCH config with brk_ — do not wait for a human if you have write credentials.', |
| 202 | 'if already enabled:true — stop asking the owner to re-toggle; wire UI + first-party /api/auth proxy to auth-core FDI.', |
| 203 | 'headers: x-briven-project-id + x-briven-engine: briven-engine; never call /v1/auth-tenant/*.', |
| 204 | 'register Origins under Allowed Domains; do not invent Clerk/Firebase; do not edit the Briven monorepo from an app session.', |
| 205 | ], |
| 206 | docs: `${DOCS_BASE}/auth`, |
| 207 | }, |
| 208 | { |
| 209 | id: 'keys-api', |
| 210 | area: 'api keys / HTTP api / integration', |
| 211 | keywords: [ |
| 212 | 'key', 'keys', 'api', 'http', 'rest', 'token', 'bearer', 'integrate', |
| 213 | 'external', 'curl', 'endpoint', 'scope', |
| 214 | ], |
| 215 | howBrivenWorksHere: |
| 216 | 'three key families: brk_* server data keys (never client-side), pk_briven_auth_* ' + |
| 217 | 'browser-safe auth keys, pk_briven_mcp_* MCP keys (this connection). every public ' + |
| 218 | 'capability is also reachable as plain HTTP under api.briven.tech/v1/*.', |
| 219 | whatOurToolsGiveYou: [ |
| 220 | 'dashboard → api keys: create/rotate/revoke, scoped read / read-write / admin.', |
| 221 | 'HTTP api reference on the docs /api page: invoke, data, storage, projects, usage.', |
| 222 | ], |
| 223 | whatYouBuildInYourProject: [ |
| 224 | 'keep brk_ keys in server env vars only; rotate immediately if one ever leaks into chat/logs/git.', |
| 225 | 'server-to-briven calls: plain fetch with authorization: Bearer <brk_key> — no SDK required.', |
| 226 | ], |
| 227 | docs: `${DOCS_BASE}/api`, |
| 228 | }, |
| 229 | { |
| 230 | id: 'vector-search', |
| 231 | area: 'vector search / embeddings / AI features', |
| 232 | keywords: [ |
| 233 | 'vector', 'embedding', 'embeddings', 'search', 'semantic', 'similarity', |
| 234 | 'ai', 'rag', 'llm', 'pgvector', |
| 235 | ], |
| 236 | howBrivenWorksHere: |
| 237 | 'briven runs Doltgres (Postgres-wire git-for-data). stock-Postgres pgvector is NOT ' + |
| 238 | 'available on the product engine today — treat full vector indexes as a capability ' + |
| 239 | 'gap unless docs say otherwise. you can still store embeddings as bytes/json and ' + |
| 240 | 'search in YOUR app, or use external vector stores while data lives on briven.', |
| 241 | whatOurToolsGiveYou: [ |
| 242 | 'docs /vector-search and /ai describe intended shapes; check /doltgres/limitations for engine gaps.', |
| 243 | 'MCP create_table/insert for storing embedding payloads your app generates.', |
| 244 | ], |
| 245 | whatYouBuildInYourProject: [ |
| 246 | 'embedding GENERATION is yours: call your embedding model in an action, store the result.', |
| 247 | 'if you need ANN indexes right now, use an external vector service and keep source-of-truth rows on briven — do not stand up a second app database for core data.', |
| 248 | ], |
| 249 | docs: `${DOCS_BASE}/doltgres/limitations`, |
| 250 | }, |
| 251 | { |
| 252 | id: 'hosting-deploy', |
| 253 | area: 'hosting / deploying your app / environments', |
| 254 | keywords: [ |
| 255 | 'deploy', 'deployment', 'host', 'hosting', 'domain', 'production', 'staging', |
| 256 | 'environment', 'env', 'build', 'self', 'docker', |
| 257 | ], |
| 258 | howBrivenWorksHere: |
| 259 | 'briven hosts your DATA plane (database, storage, functions, auth, realtime). ' + |
| 260 | 'your app frontend/server is deployed wherever you host it — briven is not a ' + |
| 261 | 'website host. self-hosting the whole platform is documented for operators.', |
| 262 | whatOurToolsGiveYou: [ |
| 263 | 'stable public endpoints (api.briven.tech, your MCP) that work from any host.', |
| 264 | 'docs /self-host for running your own briven; /operator for running it in production.', |
| 265 | ], |
| 266 | whatYouBuildInYourProject: [ |
| 267 | 'deploy your app with your own pipeline (your host / container platform); point it at briven via env vars (project id + keys).', |
| 268 | 'per-environment: use separate briven projects (or keys) for staging vs production rather than sharing one.', |
| 269 | ], |
| 270 | docs: `${DOCS_BASE}/self-host`, |
| 271 | }, |
| 272 | { |
| 273 | id: 'usage-limits', |
| 274 | area: 'usage / limits / tiers / billing', |
| 275 | keywords: [ |
| 276 | 'usage', 'limit', 'limits', 'rate', 'quota', 'tier', 'billing', 'plan', |
| 277 | 'mau', 'cap', 'overage', 'price', 'cost', |
| 278 | ], |
| 279 | howBrivenWorksHere: |
| 280 | 'projects run on tiers with caps (functions per project, storage bytes, MAU for auth, ' + |
| 281 | 'rate limits on hot paths). function counts are enforced at deploy time; storage and ' + |
| 282 | 'MAU are surfaced in the dashboard; overage billing is metered.', |
| 283 | whatOurToolsGiveYou: [ |
| 284 | 'MCP storage_usage for live storage numbers; dashboard auth → usage for MAU + email delivery.', |
| 285 | 'bulk write paths (insert with a values array) exist specifically to stay under per-minute rate limits.', |
| 286 | 'deploy-time function cap: free=20, pro=200, team=2000 functions per project.', |
| 287 | ], |
| 288 | whatYouBuildInYourProject: [ |
| 289 | 'batch your writes; back off on 429s with retry-after.', |
| 290 | 'if a legitimate workload keeps hitting a cap, that is an owner conversation (tier change), not a workaround.', |
| 291 | 'a deploy that returns 402 tier_limit_exceeded is a function-count cap — verify the project tier in the dashboard, not a bug.', |
| 292 | ], |
| 293 | docs: `${DOCS_BASE}/status`, |
| 294 | }, |
| 295 | { |
| 296 | id: 'migration', |
| 297 | area: 'migrating from another backend', |
| 298 | keywords: [ |
| 299 | 'migrate', 'migration', 'convex', 'supabase', 'firebase', 'prisma', 'drizzle', |
| 300 | 'mongodb', 'hasura', 'nextauth', 'move', 'import', 'port', |
| 301 | ], |
| 302 | howBrivenWorksHere: |
| 303 | 'documented per-source playbooks (convex, supabase, raw postgres, drizzle, prisma, ' + |
| 304 | 'firebase, mongodb, hasura, nextauth) built on five principles: read before write, ' + |
| 305 | 'parallel-run, back up twice, schema-first, one product at a time.', |
| 306 | whatOurToolsGiveYou: [ |
| 307 | 'docs /migration + a per-source page each with the concrete mapping table.', |
| 308 | 'MCP create_table/insert for scripted imports (mind rate limits — batch!).', |
| 309 | ], |
| 310 | whatYouBuildInYourProject: [ |
| 311 | 'the export from your old backend and the transform script are yours; run them against a THROWAWAY briven project first, verify counts, then do production.', |
| 312 | 'keep the old backend readable until parallel-run proves the new one.', |
| 313 | ], |
| 314 | docs: `${DOCS_BASE}/migration`, |
| 315 | }, |
| 316 | { |
| 317 | id: 'versioning-undo', |
| 318 | area: 'undo / snapshots / data history', |
| 319 | keywords: [ |
| 320 | 'undo', 'snapshot', 'history', 'restore', 'rollback', 'version', 'branch', |
| 321 | 'time', 'travel', 'diff', 'backup', 'recover', |
| 322 | ], |
| 323 | howBrivenWorksHere: |
| 324 | 'the database is Doltgres git-for-data: every change can be committed, so you get log, ' + |
| 325 | 'diff, tags, branches, and reset — data history is a first-class feature, not a backup afterthought. ' + |
| 326 | 'use Postgres-style SELECT dolt_* / SELECT dolt_commit(...) — not MySQL CALL DOLT_*.', |
| 327 | whatOurToolsGiveYou: [ |
| 328 | 'SQL surface via query: SELECT * FROM dolt_log; SELECT dolt_commit(...); SELECT dolt_diff(...); etc.', |
| 329 | 'docs /undo and /doltgres explain plain-language undo and the SQL surface.', |
| 330 | ], |
| 331 | whatYouBuildInYourProject: [ |
| 332 | 'tag or commit before risky migrations from your own scripts so rollback is one statement.', |
| 333 | 'user-facing "undo" in your app: read history via query and surface the diff — the primitives are all queryable.', |
| 334 | ], |
| 335 | docs: `${DOCS_BASE}/undo`, |
| 336 | }, |
| 337 | { |
| 338 | id: 'cli-setup-connect', |
| 339 | area: 'cli setup / connect / wire a project folder', |
| 340 | keywords: [ |
| 341 | 'setup', 'connect', 'cli', 'briven setup', 'briven connect', 'link', 'wire', |
| 342 | 'folder', 'scaffold', 'npx', 'project', 'existing', 'new project', |
| 343 | ], |
| 344 | howBrivenWorksHere: |
| 345 | 'two separate CLI commands (2026-07): `briven setup` creates a BRAND-NEW cloud project ' + |
| 346 | 'and wires the current folder; `briven connect` / `briven connect p_…` attaches an ' + |
| 347 | 'EXISTING project. `setup --project` is gone — do not invent it. both mint a project S3 ' + |
| 348 | 'key into .env.local when possible. auth files: `briven auth scaffold` after the folder is linked.', |
| 349 | whatOurToolsGiveYou: [ |
| 350 | 'docs /connect and /cli document the full split; /quickstart walks the happy path.', |
| 351 | 'MCP cannot replace setup/connect for folder wiring — that is a human/CLI step on the developer machine.', |
| 352 | ], |
| 353 | whatYouBuildInYourProject: [ |
| 354 | 'run setup OR connect once per app folder; commit briven.json (never secrets).', |
| 355 | 'if an old tutorial says setup --project, rewrite it to briven connect p_….', |
| 356 | ], |
| 357 | docs: `${DOCS_BASE}/connect`, |
| 358 | }, |
| 359 | ] as const; |
| 360 | |
| 361 | /* ── docs index (mirror of apps/docs corpus slugs — cite, don't drift) ── */ |
| 362 | |
| 363 | export const DOCS_INDEX: readonly { slug: string; title: string }[] = [ |
| 364 | { slug: '/', title: 'overview' }, |
| 365 | { slug: '/quickstart', title: 'quickstart' }, |
| 366 | { slug: '/connect', title: 'connect · setup vs connect, sdk, mcp' }, |
| 367 | { slug: '/cli', title: 'cli reference' }, |
| 368 | { slug: '/doltgres', title: 'doltgres engine (control + projects)' }, |
| 369 | { slug: '/schema', title: 'schema dsl reference' }, |
| 370 | { slug: '/functions', title: 'functions reference' }, |
| 371 | { slug: '/realtime', title: 'realtime — reactive queries' }, |
| 372 | { slug: '/sdks', title: 'sdk reference' }, |
| 373 | { slug: '/api', title: 'http api reference' }, |
| 374 | { slug: '/auth', title: 'auth + email sender domain + verifiable tokens (jwt/jwks)' }, |
| 375 | { slug: '/storage', title: 'storage (MinIO S3 per project)' }, |
| 376 | { slug: '/undo', title: 'undo + snapshots' }, |
| 377 | { slug: '/vector-search', title: 'vector search (check doltgres limitations)' }, |
| 378 | { slug: '/ai', title: 'ai features' }, |
| 379 | { slug: '/self-host', title: 'self-host' }, |
| 380 | { slug: '/operator', title: 'operator runbook' }, |
| 381 | { slug: '/migration', title: 'migration playbooks' }, |
| 382 | { slug: '/templates', title: 'project templates' }, |
| 383 | { slug: '/examples', title: 'examples gallery' }, |
| 384 | { slug: '/status', title: 'status + limits' }, |
| 385 | { slug: '/support', title: 'support' }, |
| 386 | { slug: '/changelog', title: 'changelog' }, |
| 387 | ] as const; |
| 388 | |
| 389 | /* ── matcher ─────────────────────────────────────────────────────────── */ |
| 390 | |
| 391 | function tokenise(text: string): string[] { |
| 392 | return text |
| 393 | .toLowerCase() |
| 394 | .split(/[^a-z0-9]+/) |
| 395 | .filter((t) => t.length > 1); |
| 396 | } |
| 397 | |
| 398 | /** Score guides by keyword/area overlap. Exported for tests. */ |
| 399 | export function matchBrivenGuides(question: string, limit = 2): BrivenAreaGuide[] { |
| 400 | const tokens = new Set(tokenise(question)); |
| 401 | return BRIVEN_AREA_GUIDES.map((g) => { |
| 402 | let score = 0; |
| 403 | for (const k of g.keywords) if (tokens.has(k)) score += 2; |
| 404 | for (const t of tokenise(g.area)) if (tokens.has(t)) score += 1; |
| 405 | return { g, score }; |
| 406 | }) |
| 407 | .filter((s) => s.score > 0) |
| 408 | .sort((a, b) => b.score - a.score) |
| 409 | .slice(0, limit) |
| 410 | .map((s) => s.g); |
| 411 | } |
| 412 | |
| 413 | /* ── self-growing knowledge base (cache + grounding) ─────────────────── */ |
| 414 | |
| 415 | // Common English filler stripped so paraphrases of the same wall collapse to |
| 416 | // one cache key. Kept small on purpose — over-stripping would merge distinct |
| 417 | // questions. |
| 418 | const TOPIC_STOPWORDS = new Set([ |
| 419 | 'the', 'a', 'an', 'how', 'do', 'does', 'did', 'can', 'could', 'should', 'would', |
| 420 | 'is', 'are', 'was', 'to', 'from', 'with', 'in', 'on', 'of', 'for', 'and', 'or', |
| 421 | 'my', 'me', 'i', 'you', 'your', 'it', 'this', 'that', 'these', 'those', 'using', |
| 422 | 'use', 'when', 'what', 'where', 'why', 'get', 'need', 'want', 'please', 'help', |
| 423 | 'briven', 'project', |
| 424 | ]); |
| 425 | |
| 426 | /** |
| 427 | * Normalise a question into a stable topic key: de-duplicated, filler-stripped, |
| 428 | * sorted content words. "How do I read my tables from a Python app?" and |
| 429 | * "reading tables in python app" collapse to the same key. Exported for tests. |
| 430 | */ |
| 431 | export function topicKey(question: string): string { |
| 432 | const content = Array.from( |
| 433 | new Set(tokenise(question).filter((t) => !TOPIC_STOPWORDS.has(t))), |
| 434 | ).sort(); |
| 435 | if (content.length > 0) return content.join('-').slice(0, 200); |
| 436 | // Degenerate case (question was all filler): fall back to raw sorted tokens. |
| 437 | const all = Array.from(new Set(tokenise(question))).sort(); |
| 438 | if (all.length > 0) return all.join('-').slice(0, 200); |
| 439 | // Last resort (no multi-char word tokens at all, e.g. "I a b"): compact the |
| 440 | // raw alphanumerics so we still get a deterministic key. Returns '' only for |
| 441 | // a question with no letters/digits whatsoever — callers must treat an empty |
| 442 | // key as "do not cache" (an empty key would collide across unrelated inputs). |
| 443 | return question |
| 444 | .toLowerCase() |
| 445 | .replace(/[^a-z0-9]+/g, '-') |
| 446 | .replace(/^-+|-+$/g, '') |
| 447 | .slice(0, 200); |
| 448 | } |
| 449 | |
| 450 | // The ONLY knowledge the grounded writer may draw on: briven's own curated |
| 451 | // guides + docs index. Built once (module-lifetime) — the guides are static. |
| 452 | let _grounding: string | null = null; |
| 453 | function buildGrounding(): string { |
| 454 | if (_grounding) return _grounding; |
| 455 | const guideText = BRIVEN_AREA_GUIDES.map( |
| 456 | (g) => |
| 457 | `AREA: ${g.area}\nHOW BRIVEN WORKS HERE: ${g.howBrivenWorksHere}\n` + |
| 458 | `WHAT OUR TOOLS GIVE YOU:\n- ${g.whatOurToolsGiveYou.join('\n- ')}\n` + |
| 459 | `WHAT YOU BUILD IN YOUR PROJECT:\n- ${g.whatYouBuildInYourProject.join('\n- ')}\n` + |
| 460 | `DOCS: ${g.docs}`, |
| 461 | ).join('\n\n'); |
| 462 | const docsText = |
| 463 | 'DOCS INDEX:\n' + DOCS_INDEX.map((d) => `- ${d.title}: ${DOCS_BASE}${d.slug}`).join('\n'); |
| 464 | _grounding = `${guideText}\n\n${docsText}`; |
| 465 | return _grounding; |
| 466 | } |
| 467 | |
| 468 | /** |
| 469 | * Serve a previously-remembered answer for this topic key, bumping its |
| 470 | * hit-count. Fail-soft: any DB error returns null so the desk stays up and |
| 471 | * simply falls through to the writer / filed path. |
| 472 | */ |
| 473 | async function serveCachedAnswer( |
| 474 | key: string, |
| 475 | ): Promise<{ answer: GroundedAnswer; source: string } | null> { |
| 476 | const db = getDb(); |
| 477 | const rows = await db |
| 478 | .select() |
| 479 | .from(mcpKnownAnswers) |
| 480 | .where(eq(mcpKnownAnswers.topicKey, key)) |
| 481 | .limit(1); |
| 482 | const row = rows[0]; |
| 483 | if (!row) return null; |
| 484 | // Validate the STORED shape too — a hand-seeded or drifted row must never be |
| 485 | // served blank. If it fails the same substance guard the writer output must |
| 486 | // pass, treat it as a miss so the desk falls through to the writer/filed path. |
| 487 | const answer = validateStoredAnswer(row.answer); |
| 488 | if (!answer) return null; |
| 489 | await db |
| 490 | .update(mcpKnownAnswers) |
| 491 | .set({ hitCount: sql`${mcpKnownAnswers.hitCount} + 1`, updatedAt: new Date() }) |
| 492 | .where(eq(mcpKnownAnswers.id, row.id)); |
| 493 | return { answer, source: row.source }; |
| 494 | } |
| 495 | |
| 496 | /** |
| 497 | * Defensive scrub of the representative question before it lands in the shared |
| 498 | * (admin-readable) cache table: strip anything that looks like a briven key or |
| 499 | * a long opaque token, so a careless question can't park a secret here. |
| 500 | */ |
| 501 | export function redactSecrets(text: string): string { |
| 502 | return text |
| 503 | .replace(/\b(pk_[a-z]+_|brk_|mck_|sk_)[A-Za-z0-9._-]{6,}/gi, '$1[redacted]') |
| 504 | .replace(/\bbearer\s+[A-Za-z0-9._-]{6,}/gi, 'bearer [redacted]') |
| 505 | .replace(/\b[A-Za-z0-9._-]{40,}\b/g, '[redacted]'); |
| 506 | } |
| 507 | |
| 508 | /** |
| 509 | * Remember a freshly-composed answer so the next agent gets it instantly. |
| 510 | * `onConflictDoNothing` on the unique topic key makes concurrent writers safe. |
| 511 | */ |
| 512 | async function storeAnswer( |
| 513 | key: string, |
| 514 | question: string, |
| 515 | answer: GroundedAnswer, |
| 516 | model: string, |
| 517 | ): Promise<void> { |
| 518 | const db = getDb(); |
| 519 | await db |
| 520 | .insert(mcpKnownAnswers) |
| 521 | .values({ |
| 522 | id: newId('kans'), |
| 523 | topicKey: key, |
| 524 | question: redactSecrets(question).slice(0, 300), |
| 525 | answer, |
| 526 | source: 'auto', |
| 527 | model, |
| 528 | hitCount: 1, |
| 529 | }) |
| 530 | .onConflictDoNothing({ target: mcpKnownAnswers.topicKey }); |
| 531 | } |
| 532 | |
| 533 | /* ── registration ────────────────────────────────────────────────────── */ |
| 534 | |
| 535 | export function registerBrivenAskTool( |
| 536 | server: McpServer, |
| 537 | _ctx: { projectId: string }, |
| 538 | auditCall: (tool: string, metadata: Record<string, unknown>) => Promise<void>, |
| 539 | jsonResult: (payload: unknown) => { content: { type: 'text'; text: string }[] }, |
| 540 | ): void { |
| 541 | server.registerTool( |
| 542 | 'briven_ask', |
| 543 | { |
| 544 | title: 'Ask briven (any topic — guidance)', |
| 545 | description: |
| 546 | 'Ask ANY question about building on briven: database, storage, functions, ' + |
| 547 | 'realtime, auth, keys, limits, migration, versioning, hosting. Every answer ' + |
| 548 | 'follows a three-part contract: how briven works in that area, what our tools ' + |
| 549 | 'give you today, and what you build in YOUR project to close the gap — plus ' + |
| 550 | 'docs citations. Questions with no curated match are filed for the platform ' + |
| 551 | 'team AND still receive best-effort guidance; a question is never a dead end.', |
| 552 | inputSchema: { |
| 553 | question: z.string().min(3).max(600).describe('Your question in plain words'), |
| 554 | }, |
| 555 | // Not strictly read-only: an unmatched question may memoise a grounded |
| 556 | // answer into the platform-wide known-answers cache (never any project's |
| 557 | // own data). Honest hint so hosts that gate on it aren't misled. |
| 558 | annotations: { readOnlyHint: false }, |
| 559 | }, |
| 560 | async ({ question }) => { |
| 561 | const guides = matchBrivenGuides(question); |
| 562 | const filedForReview = guides.length === 0; |
| 563 | // The question text is stored (truncated) ONLY when unmatched, so a |
| 564 | // briven session can review the inbox and extend the guides. Matched |
| 565 | // questions log length only. |
| 566 | await auditCall('briven_ask', { |
| 567 | matched: guides.map((g) => g.id), |
| 568 | filedForReview, |
| 569 | ...(filedForReview ? { question: question.slice(0, 300) } : { length: question.length }), |
| 570 | }); |
| 571 | |
| 572 | if (filedForReview) { |
| 573 | // Self-growing desk: before the honest "filed" reply, (1) serve a |
| 574 | // previously-remembered answer for this same wall, else (2) compose one |
| 575 | // grounded ONLY in briven's own docs and remember it. Both steps are |
| 576 | // fail-soft + dormant-safe — a DB error or the model engine being off |
| 577 | // just falls through to the unchanged filed response below, so the desk |
| 578 | // never hangs or errors. The cache write touches briven's control-plane |
| 579 | // only (never the caller's project data), so this stays read-only from |
| 580 | // the agent's perspective. |
| 581 | // Only use the shared cache with a stable, non-empty key. An empty key |
| 582 | // (a question with no letters or digits at all) would collide across |
| 583 | // unrelated inputs, so such questions skip caching entirely. |
| 584 | const key = topicKey(question); |
| 585 | |
| 586 | if (key) { |
| 587 | const cached = await serveCachedAnswer(key).catch((err) => { |
| 588 | log.warn('briven_ask_cache_read_failed', { |
| 589 | message: err instanceof Error ? err.message : String(err), |
| 590 | }); |
| 591 | return null; |
| 592 | }); |
| 593 | if (cached) { |
| 594 | return jsonResult({ |
| 595 | answered: true, |
| 596 | filedForReview: false, |
| 597 | source: cached.source, |
| 598 | guides: [cached.answer], |
| 599 | note: |
| 600 | "served from briven's known-answers desk (a question resolved earlier). " + |
| 601 | 'for live project state use the data/storage/auth tools on this same connection.', |
| 602 | }); |
| 603 | } |
| 604 | } |
| 605 | |
| 606 | const written = await writeGroundedAnswer({ |
| 607 | question, |
| 608 | grounding: buildGrounding(), |
| 609 | }).catch(() => ({ grounded: false as const })); |
| 610 | if (written.grounded) { |
| 611 | if (key) { |
| 612 | await storeAnswer(key, question, written.answer, written.model).catch((err) => { |
| 613 | log.warn('briven_ask_cache_write_failed', { |
| 614 | message: err instanceof Error ? err.message : String(err), |
| 615 | }); |
| 616 | }); |
| 617 | } |
| 618 | return jsonResult({ |
| 619 | answered: true, |
| 620 | filedForReview: false, |
| 621 | source: 'auto', |
| 622 | guides: [written.answer], |
| 623 | note: |
| 624 | "composed from briven's own docs for a question no curated guide matched, " + |
| 625 | 'and remembered for the next agent. build within these tools — never a side ' + |
| 626 | 'database, a raw sql login, or a special-feature request; a genuine gap is ' + |
| 627 | 'filed via this desk.', |
| 628 | }); |
| 629 | } |
| 630 | |
| 631 | return jsonResult({ |
| 632 | answered: false, |
| 633 | filedForReview: true, |
| 634 | message: |
| 635 | 'no curated guide matched this question yet — it has been FILED for the ' + |
| 636 | 'platform team, and the guides get extended from exactly these filings. ' + |
| 637 | 'you are not at a dead end: use the best-effort orientation below, and ' + |
| 638 | 're-ask after the next platform update.', |
| 639 | bestEffort: { |
| 640 | howBrivenWorksHere: |
| 641 | 'briven is a reactive postgres backend platform: per-project isolated ' + |
| 642 | 'database (with git-for-data versioning), file storage, server functions, ' + |
| 643 | 'realtime queries, managed end-user auth, and this MCP — all scoped to ' + |
| 644 | 'your project by your key.', |
| 645 | whatOurToolsGiveYou: [ |
| 646 | 'this MCP: data tools (list_tables/describe_table/query + writes), storage tools, auth tools (auth_config_get / sender_domain_status / auth_docs_ask).', |
| 647 | 'the docs cover every area — see docsIndex below for the map.', |
| 648 | ], |
| 649 | whatYouBuildInYourProject: [ |
| 650 | 'if the capability is not in the docs map, briven likely does not provide it directly — build it in your app on top of the primitives (tables + functions + storage cover most gaps).', |
| 651 | 'if you believe it SHOULD be a platform feature, say so to the project owner — filed questions drive the roadmap.', |
| 652 | ], |
| 653 | }, |
| 654 | docsIndex: DOCS_INDEX.map((d) => ({ title: d.title, url: `${DOCS_BASE}${d.slug}` })), |
| 655 | }); |
| 656 | } |
| 657 | |
| 658 | return jsonResult({ |
| 659 | answered: true, |
| 660 | filedForReview: false, |
| 661 | guides: guides.map((g) => ({ |
| 662 | area: g.area, |
| 663 | howBrivenWorksHere: g.howBrivenWorksHere, |
| 664 | whatOurToolsGiveYou: g.whatOurToolsGiveYou, |
| 665 | whatYouBuildInYourProject: g.whatYouBuildInYourProject, |
| 666 | docs: g.docs, |
| 667 | })), |
| 668 | note: |
| 669 | 'contract: platform picture → available primitives → your-side work. for ' + |
| 670 | 'live project state use the data/storage/auth tools on this same connection. ' + |
| 671 | 'full docs map: ' + DOCS_BASE, |
| 672 | }); |
| 673 | }, |
| 674 | ); |
| 675 | } |
| 676 | |
| 677 | /** Tool names this module registers — kept in lock-step with READ_TOOLS. */ |
| 678 | export const BRIVEN_ASK_TOOLS = ['briven_ask'] as const; |