{
  "markdown": "# valv\n\n**Let agents query your database. Just not all of it.**\n\n[![npm](https://img.shields.io/npm/v/@valv/core?label=%40valv%2Fcore)](https://www.npmjs.com/package/@valv/core) [![npm](https://img.shields.io/npm/v/@valv/clickhouse?label=%40valv%2Fclickhouse)](https://www.npmjs.com/package/@valv/clickhouse) [![npm](https://img.shields.io/npm/v/@valv/prisma?label=%40valv%2Fprisma)](https://www.npmjs.com/package/@valv/prisma) [![npm](https://img.shields.io/npm/v/@valv/mcp?label=%40valv%2Fmcp)](https://www.npmjs.com/package/@valv/mcp) [![license](https://img.shields.io/npm/l/@valv/core)](./LICENSE)\n\nvalv gives an agent structured tools to **read** your database — and, opt-in, to **write** to it. The model emits a **structured query** (or insert/update/delete) — never a native database command — and valv validates it against your schema, scopes it to the current user with policies you write in code, compiles it for your database, and runs it.\n\nThe model's query is treated as fully untrusted. It can't read a column you hid, a row the user isn't allowed to see, call a function you didn't allow, write a column you didn't permit, or escape its tenant on a write. Valv rebuilds and checks the query on the server before it reaches the database adapter.\n\n```ts\nconst valv = await createValv(client, { schema: \"introspect\", defaultPolicy: \"deny-all\" })\n\nvalv.policy(\"orders\", (ctx) => ({\n  read:   { tenant_id: ctx.tenant.id },   // every read is scoped to this tenant\n  fields: { deny: [\"internal_notes\"] },   // this column never reaches the model\n}))\n\nconst tools = await valv.tools.aisdk(ctx)  // hand to your agent — it queries safely\n```\n\n---\n\n## Two ways to use it\n\n- **In your app.** Configure valv in code, write policies against your request context, and hand the tools to your agent — Vercel AI SDK, Anthropic, OpenAI, or Gemini. Or expose those same tools over MCP with [`@valv/mcp-sdk`](packages/mcp-sdk), scoped per request.\n- **With a coding agent.** Point [`@valv/mcp`](packages/mcp) at a database and a tool like Claude Code queries it safely — no code required.\n\n---\n\n## Quick start\n\nInstall an adapter for your database (it pulls in `@valv/core`):\n\n```bash\nnpm i @valv/clickhouse @clickhouse/client     # ClickHouse\n# or\nnpm i @valv/prisma @prisma/client             # Postgres / MySQL / SQLite\n# or\nnpm i @valv/mongodb mongodb                    # MongoDB\n```\n\nWire it up — connect, write a policy, hand the tools to an agent:\n\n```ts\nimport { createValv } from \"@valv/clickhouse\"\nimport { generateText, stepCountIs } from \"ai\"\n\n// 1. Connect — introspect the live schema (or pass a hand-defined one).\nconst valv = await createValv(client, { schema: \"introspect\", defaultPolicy: \"deny-all\" })\n\n// 2. Policy — what this caller may read, resolved from your context.\nvalv.policy(\"orders\", (ctx) => ({ read: { tenant_id: ctx.tenant.id } }))\n\n// 3. Tools — bound to the request's context, formatted for your provider.\nconst ctx = { user: { id: \"u1\", role: \"analyst\" }, tenant: { id: \"acme\" } }\nconst { text } = await generateText({\n  model,\n  system: await valv.instructions(ctx),  // how to drive the tools + the caller's resources\n  tools: await valv.tools.aisdk(ctx),\n  stopWhen: stepCountIs(6),\n  prompt: \"What's our revenue per order status this month?\",\n})\n```\n\nThe agent gets four tools — `list_resources`, `search_resources`, `describe_resource`, and `query` — discovers your schema, and runs a query. valv scopes it to `acme`, compiles it to ClickHouse SQL, runs it, and hands back rows.\n\n---\n\n## What the agent can express\n\nOne `query` tool covers the whole read surface. The grammar is **Prisma-idiomatic** — a shape models already know cold — and desugars server-side into a checked query:\n\n```jsonc\n{\n  \"from\": \"orders\",\n  \"select\": {\n    \"status\": true,                       // a plain column\n    \"orders\": { \"count\": true },          // count(*) — the key names the output\n    \"revenue\": { \"sum\": \"total\" }         // an aggregate\n  },\n  \"where\": { \"created_at\": { \"gte\": \"2026-06-01\" } },\n  \"groupBy\": [\"status\"],\n  \"orderBy\": { \"revenue\": \"desc\" },\n  \"take\": 10\n}\n```\n\nThat's enough for real analytics — **filters** (`{ field: value }` equality, operator objects like `{ gte, lt, in, contains }`, and `AND`/`OR`/`NOT` trees), **aggregates**, **time-series** (bucket with a function and group by the alias), **top-N** (order by an aggregate), and **conditional aggregation** (`countIf`, `sumIf`). ClickHouse adds dialect functions like `quantileTiming` and `toStartOfInterval`; every function is type-checked and its literals parameterized.\n\n### Joins\n\nTo read a related resource, reference its column with a **dotted path** from the root. The model can only follow relations declared in your schema; valv derives the joins, picks the keys, and **composes the policy of every table it touches** — each joined table is scoped by its own policy and field allowlist, so a join can never reach a hidden column or another tenant's rows.\n\n```jsonc\n{\n  \"from\": \"orders\",\n  \"select\": {\n    \"customer_name\": { \"col\": \"customer.name\" },        // one hop: orders → customer\n    \"region\": { \"col\": \"customer.region.name\" },        // multi-hop: → customer → region\n    \"revenue\": { \"sum\": \"total\" }\n  },\n  \"groupBy\": [\"customer.name\"]\n}\n```\n\n`belongsTo` and `hasMany` relations are supported; join depth, table count, and fan-out are capped, and every query runs under a statement timeout. Relations are auto-introspected on Prisma and declared in the schema on ClickHouse.\n\n---\n\n## Usage\n\n### Connect\n\n`createValv` is async — it loads the schema on construction, so the instance is ready to use. Call it **once** at startup.\n\n```ts\n// ClickHouse — introspect, or hand-define a schema\nconst valv = await createValv(clickhouseClient, { schema: \"introspect\", database: \"analytics\" })\n\n// Prisma (Postgres / MySQL / SQLite / Cockroach) — schema comes from your .prisma file\nimport { createValv } from \"@valv/prisma\"\nconst valv = await createValv(prismaClient)\n\n// MongoDB — merge collection validators with sampled document fields\nimport { createValv } from \"@valv/mongodb\"\nconst valv = await createValv(mongoClient.db(\"analytics\"), { schema: \"introspect\" })\n```\n\n`defaultPolicy: \"deny-all\"` (recommended) makes a resource invisible until you write a policy for it.\n\n### Policy\n\nA policy is a function of your context. It decides what the caller may read, per resource:\n\n```ts\nvalv.policy(\"orders\", (ctx) => ({\n  read:   { tenant_id: ctx.tenant.id },   // row filter — AND-injected into every query\n  fields: { deny: [\"internal_notes\"] },   // hide columns\n}))\n\nvalv.policy(\"users\", (ctx) => ({\n  read:   { tenant_id: ctx.tenant.id },\n  fields: ctx.user.role === \"support\" ? { deny: [\"email\"] } : undefined,\n}))\n```\n\n| `read` value | Meaning |\n|---|---|\n| `true` / `false` | allow / deny outright |\n| `{ field: value }` | a row filter, AND-ed into the query server-side |\n\nThe model can't widen or override the row filter. Valv injects it after parsing\nthe model's query and before handing the query to the database adapter. Fields\nare denied two ways: `fields.deny` (a blacklist) or `fields.allow` (a\nwhitelist). Denied and unknown columns fail with the same message, so the model\ncan't probe for hidden columns. Use `\"*\"` as the resource name for a default\npolicy.\n\nThe same policy object carries the write axes — `create`, `update`, `delete` (and `write` as a shorthand for create+update) — which default to denied. See [Writes](#writes).\n\n### Tools\n\n`valv.tools.<format>(ctx, options)` returns provider-ready tools, bound to that context. Discovery is **policy-filtered** — `list`/`search`/`describe` only surface what the caller may read.\n\n```ts\nvalv.tools.anthropic(ctx)                              // Anthropic Messages API\nvalv.tools.openai(ctx)                                 // OpenAI / compatible\nvalv.tools.gemini(ctx)                                 // Google Gemini\nawait valv.tools.aisdk(ctx)                            // Vercel AI SDK (async; needs `ai`)\nvalv.tools.neutral(ctx)                                // raw, framework-agnostic\n\nvalv.tools.anthropic(ctx, { list: false, search: false })  // drop discovery tools individually\n```\n\nThe `aisdk` format returns self-executing tools (the SDK runs them). The provider formats (`anthropic`/`openai`/`gemini`) return tool **definitions** for the API request; you dispatch a tool call with `runTool`:\n\n```ts\nconst result = await valv.runTool(call.name, call.input, ctx)\n```\n\nThe discovery tools (`list`/`search`/`describe`) are on by default; the write tools (`create`/`update`/`delete`) are **off** by default — turn them on per call:\n\n```ts\nvalv.tools.aisdk(ctx, { search: false, create: true, update: true })\n```\n\n### System prompt\n\n`await valv.instructions(ctx)` returns a drop-in system-prompt block: how to drive the tools (discover → describe → query, filters are scoped server-side) plus the resources **this caller** may read — so the model can skip the opening `list_resources` round-trip. Put it in your `system` prompt alongside the tools. The static text is also exported as `AGENT_INSTRUCTIONS` if you'd rather compose the resource list yourself.\n\n```ts\nconst system = await valv.instructions(ctx)\n```\n\n```\nYou answer questions by querying a set of resources through the provided tools. Access is\nenforced server-side: every query is scoped to what the current caller may read, so you never\nneed to add tenant/owner/permission filters yourself — a query returns only permitted rows.\n\nWorkflow:\n1. Find the resource: use list_resources / search_resources; you often already have the list below.\n2. Before querying an unfamiliar resource, call describe_resource to get its exact column names,\n   types, and relations. Don't guess column names.\n3. Query with the `query` tool. Do the work in the query — filter with `where`, aggregate with\n   functions, `groupBy`, `orderBy`, `take` — rather than pulling raw rows and reducing yourself.\n4. The grammar is Prisma-like. `select` is an object keyed by output name: `true` for a plain\n   column, { \"col\": \"path\" } to rename or reach a joined column, { fn: args } to aggregate (e.g.\n   { \"revenue\": { \"sum\": \"amount\" } }). `where` uses { field: value } for equality and\n   { field: { gte, lt, in, contains } } for operators, combined with AND/OR/NOT.\n5. Read a joined resource's column with a dotted path from the root — \"customer.name\" — in a\n   select `col` or a where key. A root column takes no dot; only declared relations join.\n\nIf a call is rejected, read the error and fix the query — don't retry the same shape.\n\nResources you can query:\n- orders — customer orders\n- customers — people who place orders\n```\n\n### Writes\n\nWrites are off until you both allow them in policy and expose the tool. Each is its own tool and its own policy axis, with stronger guarantees than reads — the model can't set columns you didn't permit, can't aim a row at another tenant, and can't run an unscoped update/delete:\n\n```ts\nvalv.policy(\"orders\", (ctx) => ({\n  read:   { tenant_id: ctx.tenant.id },\n  create: { tenant_id: ctx.tenant.id },   // tenant_id is force-set on insert\n  update: { tenant_id: ctx.tenant.id },   // AND-injected into the WHERE\n  delete: false,                          // never deletable\n  fields: { readOnly: [\"status\"] },       // readable, not writable\n}))\n\nawait valv.create({ from: \"orders\", data: { status: \"pending\", total: 1200 } }, ctx)\nawait valv.update({ from: \"orders\", data: { status: \"shipped\" }, where: { /* …Prisma filter */ } }, ctx)\n```\n\n- **`create`** force-injects the policy's owned fields (`tenant_id`) onto the row — the model can't choose, omit, or override them.\n- **`update`/`delete`** AND the policy predicate into your `where`, which is **required** (no implicit \"all rows\"). The model can only touch rows within its scope.\n- The columns a write sets are checked against a **writable** allowlist (separate from readable); scope columns, sensitive fields, and `readOnly` fields aren't writable. A `where` can only filter by columns the caller can read.\n- **Databases:** Prisma supports all three operations for PostgreSQL, MySQL,\n  SQLite, and CockroachDB. ClickHouse supports `create` only. MongoDB is\n  read-only.\n\n### Saved queries & dashboards\n\nBecause the model emits a plain query object, you can **store it and re-run it** — a dashboard that refreshes without the LLM in the loop. Replays go through the full pipeline every time, so policy is always re-applied for the *current* viewer:\n\n```ts\nawait db.saveWidget(id, { query })            // it's just JSON — persist it anywhere\n\nconst rows = await valv.run(widget.query, ctx)   // fresh data, re-scoped to ctx\nconst columns = valv.resultSchema(widget.query)  // output columns + types, without running it\n```\n\n`resultSchema` derives the output shape (`[{ name, type }]`) from the query alone — handy for driving chart config and detecting drift when the schema changes. A stored query is never trusted: it's re-validated on every replay, so it can't outlive the permissions it was created under.\n\n---\n\n## How it works\n\n```\nLLM ──emits──▶  query (structured JSON, untrusted)\n                  │\n                  ▼\n              validate     check every column/function against the catalog + policy\n                  │\n                  ▼\n              inject        AND the tenant/row filter into WHERE\n                  │\n                  ▼\n              compile       produce SQL or a native database query\n                  │\n                  ▼\n              execute  ──▶  your database  ──▶  serialized rows\n```\n\nA worked example. The agent asks for revenue per status and emits:\n\n```jsonc\n{ \"from\": \"orders\",\n  \"select\": { \"status\": true, \"revenue\": { \"sum\": \"total\" } },\n  \"groupBy\": [\"status\"] }\n```\n\nWith the policy `read: { tenant_id: ctx.tenant.id }` and `ctx.tenant.id = \"acme\"`, valv emits:\n\n```sql\nSELECT `status`, sum(`total`) AS `revenue`\nFROM `orders`\nWHERE (`tenant_id` = {p0:String})          -- ← injected; the model never wrote this\nGROUP BY `status`\n-- params: p0 = \"acme\"\n```\n\nThe model never wrote the `WHERE` clause, and it can't remove it. If it had\nselected a denied column (`internal_notes`), referenced an unknown function, or\nhidden a sensitive column inside a `sumIf` predicate, validation would have\nrejected the query before the adapter compiled it. SQL adapters bind values as\nparameters instead of concatenating strings. MongoDB emits typed aggregation\npipeline values. Safety doesn't depend on the model behaving.\n\n---\n\n## Connect a coding agent (MCP)\n\nExpose your database to an agent like **Claude Code** over the [Model Context Protocol](https://modelcontextprotocol.io) — same tools, same policy enforcement.\n\n### Zero-config server\n\n[`@valv/mcp`](packages/mcp) needs no code. Run the guided setup, which probes your database and writes the config for you:\n\n```bash\nnpx @valv/mcp init\n```\n\nOr wire it by hand in your `.mcp.json` — point it at a connection string:\n\n```json\n{\n  \"mcpServers\": {\n    \"db\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@valv/mcp\"],\n      \"env\": { \"DATABASE_URL\": \"postgresql://user:pass@localhost:5432/app\" }\n    }\n  }\n}\n```\n\nIt introspects the live schema, serves the four tools **read-only by default**, and works with Prisma-supported SQL databases, ClickHouse, and MongoDB. Narrow access with `VALV_TABLES` / `VALV_EXCLUDE`, or take full control with a `VALV_POLICY_FILE`.\n\n### In your app\n\n[`@valv/mcp-sdk`](packages/mcp-sdk) turns a valv instance *you* configure into an MCP server, with policy and per-request context in your hands:\n\n```ts\nimport { startStdioServer } from \"@valv/mcp-sdk\"\n\nconst valv = await createValv(client, { schema: \"introspect\", defaultPolicy: \"deny-all\" })\nvalv.policy(\"orders\", (ctx) => ({ read: { tenant_id: ctx.tenant.id } }))\n\nawait startStdioServer(valv, {\n  context: () => resolveIdentity(),   // resolved per request (env, headers, …)\n})\n```\n\n### Charting skill\n\n[`skills/valv`](skills/valv) is a [Claude Code skill](https://docs.claude.com/en/docs/claude-code/skills) that turns a data question into a chart: it queries through the valv MCP and renders the result as a self-contained Chart.js HTML file. Ask it to \"visualize revenue by month\" and it discovers the schema, runs one structured query, and opens the chart.\n\nIt also **learns your database as you use it**. The first time it describes a table, figures out the dialect's time-bucket function, or maps \"revenue\" to `sum(total)` on `orders`, it records that in `.valv/notes.md` in your working directory — so later sessions skip the rediscovery and start warm. The notes hold schema and semantics only, never result rows, and the file is plain markdown you can read, edit, or pre-seed yourself.\n\n---\n\n## Adapters\n\n| Package | Database | Install |\n|---|---|---|\n| [`@valv/clickhouse`](packages/clickhouse) | ClickHouse | `npm i @valv/clickhouse @clickhouse/client` |\n| [`@valv/mongodb`](packages/mongodb) | MongoDB | `npm i @valv/mongodb mongodb` |\n| [`@valv/prisma`](packages/prisma) | PostgreSQL, MySQL, SQLite, CockroachDB | `npm i @valv/prisma @prisma/client` |\n\nEverything above the adapter (the query grammar, validation, policy injection, and the tool layer) lives in `@valv/core` and is database-agnostic. Each adapter introspects its database and runs the validated, policy-injected query. SQL adapters share one emitter; the MongoDB adapter compiles the same query into an aggregation pipeline.\n\n## Examples\n\n- [`examples/hand-schema`](examples/hand-schema) — offline, no database: a\n  hand-defined schema, queries, and `resultSchema`. The fastest way to see the\n  pipeline.\n- [`examples/mongodb`](examples/mongodb) — MongoDB introspection, tenant policy,\n  field allowlisting, and a grouped aggregation.\n- [`examples/clickhouse-analytics`](examples/clickhouse-analytics) — an agent\n  answering analytics questions over ClickHouse.\n- [`examples/ecommerce`](examples/ecommerce) — an agent over Postgres (Prisma),\n  plus a [saved-query dashboard](examples/ecommerce/live-dashboard.ts).\n\n## License\n\nMIT\n",
  "bytes": 18029,
  "sha": "26478fdda5949d5b4ca64917e14347a453ffdd814737c6cd17c7c8aca5a1c287",
  "repo_slug": "valv-dev/valv",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_sh_valv_mcp_2c0012f2/readme"
}