@vistal/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @vistal/mcp
2
+
3
+ **Point it at a database URL. Claude talks to your data. No SQL, no code.**
4
+
5
+ A zero-config [MCP](https://modelcontextprotocol.io) server for any database
6
+ [vistal](https://github.com/vista-libs/vista) supports through Prisma
7
+ (PostgreSQL, MySQL, SQLite, SQL Server). Give it a `DATABASE_URL` and it:
8
+
9
+ 1. introspects your live schema (`prisma db pull`) — no `schema.prisma` needed,
10
+ 2. generates the typed tools an agent uses to query it,
11
+ 3. serves them over MCP — **read-only by default**, scoped by your policies.
12
+
13
+ No SQL ever reaches the model, and it can only do what the policy allows.
14
+
15
+ ## Quick start (Claude Code)
16
+
17
+ Add it to your `.mcp.json` — nothing to install or write:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "db": {
23
+ "command": "npx",
24
+ "args": ["-y", "@vistal/mcp"],
25
+ "env": { "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb" }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ That's it. Claude can now discover your tables (`list_resources`,
32
+ `describe_resource`) and read them (`query`, `get`, `aggregate`) — all
33
+ policy-gated, all without writing SQL.
34
+
35
+ You can also run it directly:
36
+
37
+ ```bash
38
+ DATABASE_URL="postgresql://…" npx @vistal/mcp
39
+ # or pass the URL as the first argument
40
+ npx @vistal/mcp "postgresql://…"
41
+ ```
42
+
43
+ ## Defaults & safety
44
+
45
+ - **Read-only.** Out of the box only `query` / `get` / `aggregate` are exposed —
46
+ never `create` / `update` / `delete`. Writes require a policy file (below).
47
+ - **All tables, opt-out.** Every table is exposed; narrow it with allow/deny lists.
48
+ - **No SQL, no leaks.** The model calls typed tools; vistal turns them into scoped
49
+ queries server-side. Fields marked sensitive in your schema never reach it.
50
+
51
+ ## Configuration
52
+
53
+ All via environment variables:
54
+
55
+ | Variable | Description |
56
+ | --- | --- |
57
+ | `DATABASE_URL` | Connection string (required; or pass as the first arg). |
58
+ | `VISTAL_PROVIDER` | `postgresql` \| `mysql` \| `sqlite` \| `sqlserver` \| `mongodb`. Inferred from the URL when omitted. |
59
+ | `VISTAL_TABLES` | Comma-separated allow-list. Only these tables are exposed. |
60
+ | `VISTAL_EXCLUDE` | Comma-separated deny-list, applied after the allow-list. |
61
+ | `VISTAL_POLICY_FILE` | Path to a policy module that takes full control (enables writes — see below). |
62
+ | `VISTAL_CONTEXT` | JSON policy context passed to your policy file. Defaults to `{}`. |
63
+ | `VISTAL_HTTP_PORT` | Serve over Streamable HTTP on this port instead of stdio. |
64
+
65
+ ### Enabling writes / richer policies
66
+
67
+ For anything beyond read-only, point `VISTAL_POLICY_FILE` at a CommonJS module
68
+ that receives the configured vistal instance and declares
69
+ [policies](https://github.com/vista-libs/vista#policy-reference). It takes full
70
+ control of access (the allow/deny lists are then ignored):
71
+
72
+ ```js
73
+ // db-policy.cjs
74
+ module.exports = (vistal) => {
75
+ vistal.policy("orders", () => ({ read: true, write: true, delete: false }))
76
+ vistal.policy("users", () => ({ read: true, write: false, delete: false }))
77
+ }
78
+ ```
79
+
80
+ ```json
81
+ { "env": {
82
+ "DATABASE_URL": "postgresql://…",
83
+ "VISTAL_POLICY_FILE": "./db-policy.cjs"
84
+ } }
85
+ ```
86
+
87
+ Use `VISTAL_CONTEXT` to feed runtime context (tenant, role, …) your policies read:
88
+ `"VISTAL_CONTEXT": "{\"tenant\":{\"id\":\"acme\"}}"`.
89
+
90
+ ## How it works
91
+
92
+ ```
93
+ Claude Code ─MCP─▶ @vistal/mcp
94
+ │ prisma db pull + generate → live schema + typed client
95
+ │ @vistal/core → policy engine (read-only default)
96
+
97
+ @vistal/prisma → PrismaClient → your database
98
+ ```
99
+
100
+ The generated client lives in a temp directory and is removed on shutdown; your
101
+ project is never modified. Requires network access to the database and the
102
+ ability to run the bundled Prisma CLI.
103
+
104
+ ## Need policies-in-code instead?
105
+
106
+ If you'd rather define your adapter and policies yourself and embed an MCP server
107
+ in your own app, use [`@vistal/mcp-sdk`](../mcp-sdk/README.md) directly.
108
+
109
+ ## License
110
+
111
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const config_1 = require("./config");
5
+ const index_1 = require("./index");
6
+ async function main() {
7
+ const config = (0, config_1.configFromEnv)(process.env, process.argv.slice(2));
8
+ const server = await (0, index_1.startVistalMcpServer)(config);
9
+ const shutdown = async () => {
10
+ await server.stop();
11
+ process.exit(0);
12
+ };
13
+ process.on("SIGINT", shutdown);
14
+ process.on("SIGTERM", shutdown);
15
+ }
16
+ main().catch((err) => {
17
+ // Never write errors to stdout — it's the MCP transport.
18
+ process.stderr.write(`[vistal] ${err instanceof Error ? err.message : String(err)}\n`);
19
+ process.exit(1);
20
+ });
21
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;AACA,qCAAwC;AACxC,mCAA8C;AAE9C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,IAAA,sBAAa,EAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAChE,MAAM,MAAM,GAAG,MAAM,IAAA,4BAAoB,EAAC,MAAM,CAAC,CAAA;IAEjD,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;QACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAA;IACD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC9B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;AACjC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,yDAAyD;IACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACtF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
@@ -0,0 +1,24 @@
1
+ export type Provider = "postgresql" | "mysql" | "sqlite" | "sqlserver" | "mongodb";
2
+ export interface ServerConfig {
3
+ /** Database connection string. */
4
+ databaseUrl: string;
5
+ /** Prisma datasource provider. Inferred from the URL when omitted. */
6
+ provider?: Provider;
7
+ /** Path to a policy module that takes full control of access (optional). */
8
+ policyFile?: string;
9
+ /** Allow-list of resource (table) names. When set, only these are exposed. */
10
+ tables?: string[];
11
+ /** Deny-list of resource (table) names, applied after the allow-list. */
12
+ exclude?: string[];
13
+ /** Policy context object (JSON). Defaults to `{}`. */
14
+ context?: unknown;
15
+ /** When set, serve over Streamable HTTP on this port instead of stdio. */
16
+ httpPort?: number;
17
+ }
18
+ export declare function inferProvider(url: string): Provider;
19
+ /**
20
+ * Build a {@link ServerConfig} from environment variables and CLI args.
21
+ * The connection string comes from `DATABASE_URL` or the first positional arg.
22
+ */
23
+ export declare function configFromEnv(env: NodeJS.ProcessEnv, argv: string[]): ServerConfig;
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAA;AAElF,MAAM,WAAW,YAAY;IAC3B,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAUD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAQnD;AAWD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAoClF"}
package/dist/config.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferProvider = inferProvider;
4
+ exports.configFromEnv = configFromEnv;
5
+ const PROVIDER_PATTERNS = [
6
+ [/^postgres(ql)?:\/\//i, "postgresql"],
7
+ [/^mysql:\/\//i, "mysql"],
8
+ [/^sqlserver:\/\//i, "sqlserver"],
9
+ [/^(file:|sqlite:)/i, "sqlite"],
10
+ [/^mongodb(\+srv)?:\/\//i, "mongodb"],
11
+ ];
12
+ function inferProvider(url) {
13
+ for (const [pattern, provider] of PROVIDER_PATTERNS) {
14
+ if (pattern.test(url))
15
+ return provider;
16
+ }
17
+ throw new Error(`Could not infer the database provider from the connection string. ` +
18
+ `Set VISTAL_PROVIDER to one of: postgresql, mysql, sqlite, sqlserver, mongodb.`);
19
+ }
20
+ function csv(value) {
21
+ if (!value)
22
+ return undefined;
23
+ const items = value
24
+ .split(",")
25
+ .map((s) => s.trim())
26
+ .filter(Boolean);
27
+ return items.length > 0 ? items : undefined;
28
+ }
29
+ /**
30
+ * Build a {@link ServerConfig} from environment variables and CLI args.
31
+ * The connection string comes from `DATABASE_URL` or the first positional arg.
32
+ */
33
+ function configFromEnv(env, argv) {
34
+ const positional = argv.find((a) => !a.startsWith("-"));
35
+ const databaseUrl = env.DATABASE_URL ?? positional;
36
+ if (!databaseUrl) {
37
+ throw new Error("No database connection string. Set DATABASE_URL (or pass it as the first argument).");
38
+ }
39
+ const provider = env.VISTAL_PROVIDER
40
+ ? env.VISTAL_PROVIDER
41
+ : inferProvider(databaseUrl);
42
+ let context = {};
43
+ if (env.VISTAL_CONTEXT) {
44
+ try {
45
+ context = JSON.parse(env.VISTAL_CONTEXT);
46
+ }
47
+ catch {
48
+ throw new Error("VISTAL_CONTEXT must be valid JSON.");
49
+ }
50
+ }
51
+ const httpPort = env.VISTAL_HTTP_PORT ? Number(env.VISTAL_HTTP_PORT) : undefined;
52
+ if (httpPort !== undefined && Number.isNaN(httpPort)) {
53
+ throw new Error("VISTAL_HTTP_PORT must be a number.");
54
+ }
55
+ return {
56
+ databaseUrl,
57
+ provider,
58
+ policyFile: env.VISTAL_POLICY_FILE,
59
+ tables: csv(env.VISTAL_TABLES),
60
+ exclude: csv(env.VISTAL_EXCLUDE),
61
+ context,
62
+ httpPort,
63
+ };
64
+ }
65
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;AA2BA,sCAQC;AAeD,sCAoCC;AAnED,MAAM,iBAAiB,GAAyB;IAC9C,CAAC,sBAAsB,EAAE,YAAY,CAAC;IACtC,CAAC,cAAc,EAAE,OAAO,CAAC;IACzB,CAAC,kBAAkB,EAAE,WAAW,CAAC;IACjC,CAAC,mBAAmB,EAAE,QAAQ,CAAC;IAC/B,CAAC,wBAAwB,EAAE,SAAS,CAAC;CACtC,CAAA;AAED,SAAgB,aAAa,CAAC,GAAW;IACvC,KAAK,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,iBAAiB,EAAE,CAAC;QACpD,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,QAAQ,CAAA;IACxC,CAAC;IACD,MAAM,IAAI,KAAK,CACb,oEAAoE;QAClE,+EAA+E,CAClF,CAAA;AACH,CAAC;AAED,SAAS,GAAG,CAAC,KAAyB;IACpC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,MAAM,KAAK,GAAG,KAAK;SAChB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAA;IAClB,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AAC7C,CAAC;AAED;;;GAGG;AACH,SAAgB,aAAa,CAAC,GAAsB,EAAE,IAAc;IAClE,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,GAAG,CAAC,YAAY,IAAI,UAAU,CAAA;IAClD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,qFAAqF,CACtF,CAAA;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,CAAC,eAAe;QAClC,CAAC,CAAE,GAAG,CAAC,eAA4B;QACnC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAA;IAE9B,IAAI,OAAO,GAAY,EAAE,CAAA;IACzB,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAChF,IAAI,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACvD,CAAC;IAED,OAAO;QACL,WAAW;QACX,QAAQ;QACR,UAAU,EAAE,GAAG,CAAC,kBAAkB;QAClC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC;QAC9B,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC;QAChC,OAAO;QACP,QAAQ;KACT,CAAA;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { ServerConfig } from "./config";
2
+ export type { ServerConfig, Provider } from "./config";
3
+ export { configFromEnv, inferProvider } from "./config";
4
+ export interface RunningServer {
5
+ /** Disconnect the database and remove the generated temp client. */
6
+ stop: () => Promise<void>;
7
+ }
8
+ /**
9
+ * Boot a zero-config vistal MCP server against a live database: introspect the
10
+ * schema, build a policy-gated vistal instance (read-only by default), and serve
11
+ * it over stdio (or Streamable HTTP when `httpPort` is set).
12
+ *
13
+ * All progress is logged to stderr so stdout stays a clean MCP channel.
14
+ */
15
+ export declare function startVistalMcpServer(config: ServerConfig): Promise<RunningServer>;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAM5C,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAEvD,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAuCvF"}
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferProvider = exports.configFromEnv = void 0;
4
+ exports.startVistalMcpServer = startVistalMcpServer;
5
+ const prisma_1 = require("@vistal/prisma");
6
+ const mcp_sdk_1 = require("@vistal/mcp-sdk");
7
+ const prepare_1 = require("./prepare");
8
+ const policy_1 = require("./policy");
9
+ var config_1 = require("./config");
10
+ Object.defineProperty(exports, "configFromEnv", { enumerable: true, get: function () { return config_1.configFromEnv; } });
11
+ Object.defineProperty(exports, "inferProvider", { enumerable: true, get: function () { return config_1.inferProvider; } });
12
+ /**
13
+ * Boot a zero-config vistal MCP server against a live database: introspect the
14
+ * schema, build a policy-gated vistal instance (read-only by default), and serve
15
+ * it over stdio (or Streamable HTTP when `httpPort` is set).
16
+ *
17
+ * All progress is logged to stderr so stdout stays a clean MCP channel.
18
+ */
19
+ async function startVistalMcpServer(config) {
20
+ const provider = config.provider ?? "postgresql";
21
+ const prepared = await (0, prepare_1.prepareDatabase)(config.databaseUrl, provider);
22
+ const vistal = (0, prisma_1.createVistal)(prepared.prisma, {
23
+ schemaPath: prepared.schemaPath,
24
+ defaultPolicy: "deny-all",
25
+ });
26
+ const { resources } = await (0, policy_1.applyAccess)(vistal, config);
27
+ process.stderr.write(`[vistal] Exposing ${resources.length} resource(s)` +
28
+ (config.policyFile ? " (policy file in control)" : " (read-only)") +
29
+ `: ${resources.join(", ") || "(none)"}\n`);
30
+ const options = {
31
+ context: () => config.context ?? {},
32
+ toolOptions: { resources },
33
+ };
34
+ if (config.httpPort !== undefined) {
35
+ await (0, mcp_sdk_1.startHttpServer)(vistal, { ...options, port: config.httpPort });
36
+ process.stderr.write(`[vistal] MCP server listening on http://localhost:${config.httpPort}/mcp\n`);
37
+ }
38
+ else {
39
+ await (0, mcp_sdk_1.startStdioServer)(vistal, options);
40
+ process.stderr.write("[vistal] MCP server ready on stdio.\n");
41
+ }
42
+ const stop = async () => {
43
+ try {
44
+ await prepared.prisma.$disconnect();
45
+ }
46
+ finally {
47
+ prepared.cleanup();
48
+ }
49
+ };
50
+ return { stop };
51
+ }
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AA0BA,oDAuCC;AAhED,2CAA6C;AAC7C,6CAAmE;AACnE,uCAA2C;AAC3C,qCAAsC;AAQtC,mCAAuD;AAA9C,uGAAA,aAAa,OAAA;AAAE,uGAAA,aAAa,OAAA;AAOrC;;;;;;GAMG;AACI,KAAK,UAAU,oBAAoB,CAAC,MAAoB;IAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,YAAY,CAAA;IAChD,MAAM,QAAQ,GAAG,MAAM,IAAA,yBAAe,EAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;IAEpE,MAAM,MAAM,GAAG,IAAA,qBAAY,EAAoB,QAAQ,CAAC,MAAM,EAAE;QAC9D,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,aAAa,EAAE,UAAU;KAC1B,CAAC,CAAA;IAEF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,IAAA,oBAAW,EAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACvD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qBAAqB,SAAS,CAAC,MAAM,cAAc;QACjD,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,cAAc,CAAC;QAClE,KAAK,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,IAAI,CAC5C,CAAA;IAED,MAAM,OAAO,GAAG;QACd,OAAO,EAAE,GAAQ,EAAE,CAAE,MAAM,CAAC,OAAe,IAAI,EAAE;QACjD,WAAW,EAAE,EAAE,SAAS,EAAE;KAC3B,CAAA;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,IAAA,yBAAe,EAAC,MAAM,EAAE,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;QACpE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qDAAqD,MAAM,CAAC,QAAQ,QAAQ,CAC7E,CAAA;IACH,CAAC;SAAM,CAAC;QACN,MAAM,IAAA,0BAAgB,EAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAA;IAC/D,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;QACtB,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAA;QACrC,CAAC;gBAAS,CAAC;YACT,QAAQ,CAAC,OAAO,EAAE,CAAA;QACpB,CAAC;IACH,CAAC,CAAA;IACD,OAAO,EAAE,IAAI,EAAE,CAAA;AACjB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Vistal } from "@vistal/core";
2
+ import type { ServerConfig } from "./config";
3
+ type AnyVistal = Vistal<any, any>;
4
+ /**
5
+ * Apply access rules to the vistal instance and return the set of resources to
6
+ * expose as tools. When a policy file is supplied it takes full control;
7
+ * otherwise the default is read-only (query/get/aggregate, never write/delete),
8
+ * scoped by the optional table allow/deny lists.
9
+ */
10
+ export declare function applyAccess(vistal: AnyVistal, config: ServerConfig): Promise<{
11
+ resources: string[];
12
+ }>;
13
+ export {};
14
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAG5C,KAAK,SAAS,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AA4BjC;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA2BlC"}
package/dist/policy.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAccess = applyAccess;
4
+ const node_path_1 = require("node:path");
5
+ function loadPolicyFile(path, vistal) {
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ const mod = require((0, node_path_1.resolve)(path));
8
+ const fn = typeof mod === "function" ? mod : (mod.default ?? mod.applyPolicies);
9
+ if (typeof fn !== "function") {
10
+ throw new Error(`Policy file "${path}" must export a function (default export or \`applyPolicies\`).`);
11
+ }
12
+ fn(vistal);
13
+ }
14
+ /**
15
+ * Apply access rules to the vistal instance and return the set of resources to
16
+ * expose as tools. When a policy file is supplied it takes full control;
17
+ * otherwise the default is read-only (query/get/aggregate, never write/delete),
18
+ * scoped by the optional table allow/deny lists.
19
+ */
20
+ async function applyAccess(vistal, config) {
21
+ const all = await vistal.resources();
22
+ if (config.policyFile) {
23
+ loadPolicyFile(config.policyFile, vistal);
24
+ // The policy file owns access; expose everything it permits (vistal suppresses
25
+ // tools for resources it denies). Allow/deny lists are ignored in this mode.
26
+ return { resources: all };
27
+ }
28
+ // Default: read-only everywhere.
29
+ vistal.policy("*", () => ({ read: true, aggregate: true, write: false, delete: false }));
30
+ // Table scoping: allow-list first (if given), then deny-list.
31
+ let allowed = config.tables ? all.filter((t) => config.tables.includes(t)) : all;
32
+ if (config.exclude)
33
+ allowed = allowed.filter((t) => !config.exclude.includes(t));
34
+ // Authoritatively deny everything not allowed, so a model can't reach an
35
+ // excluded table even by passing its name to a consolidated verb.
36
+ const allowedSet = new Set(allowed);
37
+ for (const name of all) {
38
+ if (!allowedSet.has(name)) {
39
+ vistal.policy(name, () => ({ read: false, write: false, delete: false }));
40
+ }
41
+ }
42
+ return { resources: allowed };
43
+ }
44
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":";;AAuCA,kCA8BC;AArED,yCAAmC;AAqBnC,SAAS,cAAc,CAAC,IAAY,EAAE,MAAiB;IACrD,8DAA8D;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,IAAA,mBAAO,EAAC,IAAI,CAAC,CAAiB,CAAA;IAClD,MAAM,EAAE,GAAG,OAAO,GAAG,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,aAAa,CAAC,CAAA;IAC/E,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,gBAAgB,IAAI,iEAAiE,CACtF,CAAA;IACH,CAAC;IACD,EAAE,CAAC,MAAM,CAAC,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,WAAW,CAC/B,MAAiB,EACjB,MAAoB;IAEpB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAA;IAEpC,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QACzC,+EAA+E;QAC/E,6EAA6E;QAC7E,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,CAAA;IAC3B,CAAC;IAED,iCAAiC;IACjC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IAExF,8DAA8D;IAC9D,IAAI,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,MAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;IACzF,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAEzF,yEAAyE;IACzE,kEAAkE;IAClE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;IACnC,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;QAC3E,CAAC;IACH,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAA;AAC/B,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { PrismaClient } from "@prisma/client";
2
+ import type { Provider } from "./config";
3
+ export interface PreparedDatabase {
4
+ prisma: PrismaClient;
5
+ schemaPath: string;
6
+ /** Remove the generated client + schema temp dir. */
7
+ cleanup: () => void;
8
+ }
9
+ /**
10
+ * Introspect a live database into a throwaway Prisma schema + client, returning
11
+ * a ready `PrismaClient`. This is what makes the server "zero-config": no
12
+ * `schema.prisma` and no generated client need to exist ahead of time.
13
+ *
14
+ * Diagnostics are written to stderr so stdout stays clean for the MCP transport.
15
+ */
16
+ export declare function prepareDatabase(databaseUrl: string, provider: Provider): Promise<PreparedDatabase>;
17
+ //# sourceMappingURL=prepare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare.d.ts","sourceRoot":"","sources":["../src/prepare.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAWxC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAqBD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,gBAAgB,CAAC,CA4C3B"}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareDatabase = prepareDatabase;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ // The node_modules directory that holds @prisma/client. We generate the throwaway
8
+ // client *inside* it so the generated runtime resolves @prisma/client normally —
9
+ // generating into an unrelated temp dir makes Prisma walk up to "/", fail to find
10
+ // the installed prisma, and attempt a detached auto-install.
11
+ function clientBaseDir() {
12
+ const pkg = require.resolve("@prisma/client/package.json");
13
+ return (0, node_path_1.dirname)((0, node_path_1.dirname)((0, node_path_1.dirname)(pkg))); // .../node_modules/@prisma/client/package.json → .../node_modules
14
+ }
15
+ // Resolve the locally-installed Prisma CLI entrypoint so we can run it with the
16
+ // current node binary — more reliable than relying on `npx` resolution.
17
+ function resolvePrismaCli() {
18
+ const pkgJsonPath = require.resolve("prisma/package.json");
19
+ const pkg = require("prisma/package.json");
20
+ const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.prisma;
21
+ if (!binRel)
22
+ throw new Error("Could not locate the Prisma CLI entrypoint.");
23
+ return (0, node_path_1.join)((0, node_path_1.dirname)(pkgJsonPath), binRel);
24
+ }
25
+ // Run a Prisma CLI command. Child stdout is routed to *our* stderr so it never
26
+ // pollutes the MCP stdio channel (which speaks JSON-RPC on stdout).
27
+ function runPrisma(cliPath, args, env) {
28
+ (0, node_child_process_1.execFileSync)(process.execPath, [cliPath, ...args], {
29
+ env,
30
+ stdio: ["ignore", 2, 2],
31
+ });
32
+ }
33
+ /**
34
+ * Introspect a live database into a throwaway Prisma schema + client, returning
35
+ * a ready `PrismaClient`. This is what makes the server "zero-config": no
36
+ * `schema.prisma` and no generated client need to exist ahead of time.
37
+ *
38
+ * Diagnostics are written to stderr so stdout stays clean for the MCP transport.
39
+ */
40
+ async function prepareDatabase(databaseUrl, provider) {
41
+ const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)(clientBaseDir(), ".vistal-mcp-"));
42
+ const schemaPath = (0, node_path_1.join)(dir, "schema.prisma");
43
+ const clientOutput = (0, node_path_1.join)(dir, "client");
44
+ const schema = `generator client {
45
+ provider = "prisma-client-js"
46
+ output = "${clientOutput.replace(/\\/g, "\\\\")}"
47
+ }
48
+
49
+ datasource db {
50
+ provider = "${provider}"
51
+ url = env("DATABASE_URL")
52
+ }
53
+ `;
54
+ (0, node_fs_1.writeFileSync)(schemaPath, schema);
55
+ const env = { ...process.env, DATABASE_URL: databaseUrl };
56
+ const cli = resolvePrismaCli();
57
+ process.stderr.write("[vistal] Introspecting database schema…\n");
58
+ runPrisma(cli, ["db", "pull", "--schema", schemaPath], env);
59
+ process.stderr.write("[vistal] Generating client…\n");
60
+ runPrisma(cli, ["generate", "--schema", schemaPath], env);
61
+ const { PrismaClient: GeneratedClient } = require(clientOutput);
62
+ // Pass the URL straight to the client. The schema's datasource reads
63
+ // env("DATABASE_URL"), which isn't set in this process — and this also avoids
64
+ // ever writing the connection string to disk.
65
+ const prisma = new GeneratedClient({ datasources: { db: { url: databaseUrl } } });
66
+ return {
67
+ prisma,
68
+ schemaPath,
69
+ cleanup: () => {
70
+ try {
71
+ (0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
72
+ }
73
+ catch {
74
+ // best-effort temp cleanup
75
+ }
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=prepare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare.js","sourceRoot":"","sources":["../src/prepare.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@vistal/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Zero-config MCP server for any database — point it at a DATABASE_URL and let coding agents (Claude Code) query it safely, no SQL and no code",
5
+ "keywords": [
6
+ "mcp",
7
+ "model context protocol",
8
+ "claude code",
9
+ "database",
10
+ "postgres",
11
+ "mysql",
12
+ "sqlite",
13
+ "prisma",
14
+ "ai agent",
15
+ "row-level security",
16
+ "cli"
17
+ ],
18
+ "author": "Nicolò D'Addabbo <nicolodaddabbo@gmail.com> (https://nicolod.com)",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/vista-libs/vista.git",
23
+ "directory": "packages/mcp"
24
+ },
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "bin": {
28
+ "vistal-mcp": "dist/cli.js"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "require": "./dist/index.js",
33
+ "types": "./dist/index.d.ts"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc"
41
+ },
42
+ "dependencies": {
43
+ "@prisma/client": "^5.0.0",
44
+ "@vistal/core": "*",
45
+ "@vistal/mcp-sdk": "*",
46
+ "@vistal/prisma": "*",
47
+ "prisma": "^5.0.0"
48
+ }
49
+ }