mcp-briven-ask.ts678 lines · main
1import { newId } from '@briven/shared';
2import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3import { eq, sql } from 'drizzle-orm';
4import { z } from 'zod';
5
6import { getDb } from '../db/client.js';
7import { mcpKnownAnswers } from '../db/schema.js';
8import { log } from '../lib/logger.js';
9import {
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
36const DOCS_BASE = 'https://docs.briven.tech';
37
38/* ── the area guides — hand-curated, three-part contract ────────────── */
39
40export 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
50export 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
363export 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
391function 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. */
399export 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.
418const 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 */
431export 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.
452let _grounding: string | null = null;
453function 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 */
473async 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 */
501export 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 */
512async 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
535export 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. */
678export const BRIVEN_ASK_TOOLS = ['briven_ask'] as const;