Skip to main content
Glama

Prisma MCP Server

Official
by prisma
Studio.ts13.8 kB
import { readFile } from 'node:fs/promises' import { serve } from '@hono/node-server' import type { PrismaConfigInternal } from '@prisma/config' import { arg, type Command, format, HelpError, isError } from '@prisma/internals' import type { Executor, SequenceExecutor } from '@prisma/studio-core-licensed/data' import { serializeError, type StudioBFFRequest } from '@prisma/studio-core-licensed/data/bff' import { createMySQL2Executor } from '@prisma/studio-core-licensed/data/mysql2' import { createNodeSQLiteExecutor } from '@prisma/studio-core-licensed/data/node-sqlite' import { createPostgresJSExecutor } from '@prisma/studio-core-licensed/data/postgresjs' import type { StudioProps } from '@prisma/studio-core-licensed/ui' import { type Check, check as sendEvent } from 'checkpoint-client' import { getPort } from 'get-port-please' import { Hono } from 'hono' import { cors } from 'hono/cors' import { bold, dim, red } from 'kleur/colors' import { digest } from 'ohash' import open from 'open' import { dirname, extname, join, resolve } from 'pathe' import { runtime } from 'std-env' import packageJson from '../package.json' assert { type: 'json' } /** * `prisma dev`'s `51_213 - 1` */ const DEFAULT_PORT = 51_212 const MIN_PORT = 49_152 const STATIC_ASSETS_DIR = join(require.resolve('@prisma/studio-core-licensed/data'), '../..') const FILE_EXTENSION_TO_CONTENT_TYPE: Record<string, string> = { '.css': 'text/css', '.js': 'application/javascript', '.mjs': 'application/javascript', '.html': 'text/html', '.htm': 'text/html', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf', '.eot': 'application/vnd.ms-fontobject', } const DEFAULT_CONTENT_TYPE = 'application/octet-stream' const ADAPTER_FILE_NAME = 'adapter.js' const ADAPTER_FACTORY_FUNCTION_NAME = 'createAdapter' interface StudioStuff { createExecutor(connectionString: string, relativeTo: string): Promise<Executor> reExportAdapterScript: string } const POSTGRES_STUDIO_STUFF: StudioStuff = { async createExecutor(connectionString) { const postgresModule = await import('postgres') const postgres = postgresModule.default(connectionString) process.once('SIGINT', () => postgres.end()) process.once('SIGTERM', () => postgres.end()) return createPostgresJSExecutor(postgres) }, reExportAdapterScript: `export { createPostgresAdapter as ${ADAPTER_FACTORY_FUNCTION_NAME} } from '/data/postgres-core/index.js';`, } type Database = { new (path: string): import('better-sqlite3').Database } const CONNECTION_STRING_PROTOCOL_TO_STUDIO_STUFF: Record<string, StudioStuff | null> = { // TODO: figure out PGLite support later. file: { async createExecutor(uri, relativeTo) { const path = uri.replace('file:', '') const resolvedPath = path !== ':memory:' ? resolve(relativeTo, path) : path let database: InstanceType<Database> | undefined = undefined try { // TODO: remove 'as' once Node.js v22 is the minimum supported version. const { DatabaseSync } = (await import('node:sqlite' as never)) as { DatabaseSync: Database } database = new DatabaseSync(resolvedPath) } catch (error: unknown) { try { switch (runtime) { case 'node': { const { default: Database } = await import('better-sqlite3') database = new Database(resolvedPath) break } case 'deno': { const { Database } = (await import('jsr:@db/sqlite@0.13.0' as never)) as { Database: Database } database = new Database(resolvedPath) break } case 'bun': { const { Database } = (await import('bun:sqlite' as never)) as { Database: Database } database = new Database(resolvedPath) as never break } default: throw new Error(`Unsupported runtime for SQLite: "${runtime}"`) } } catch (error: unknown) { throw new Error( `Failed to open SQLite database at "${resolvedPath}".\nCaused by: ${(error as Error).message} Please use Node.js >=22.5, Deno >=2.2 or Bun >=1.0 or ensure you have the \`better-sqlite3\` package installed for Node.js <22.5 or the \`jsr:@db/sqlite\` package installed for Deno <2.2.`, ) } } process.once('SIGINT', () => database!.close()) process.once('SIGTERM', () => database!.close()) return createNodeSQLiteExecutor(database) }, reExportAdapterScript: `export { createSQLiteAdapter as ${ADAPTER_FACTORY_FUNCTION_NAME} } from '/data/sqlite-core/index.js';`, }, postgres: POSTGRES_STUDIO_STUFF, postgresql: POSTGRES_STUDIO_STUFF, 'prisma+postgres': POSTGRES_STUDIO_STUFF, mysql: { async createExecutor(connectionString) { const { createPool } = await import('mysql2/promise') const pool = createPool(connectionString) process.once('SIGINT', () => pool.end()) process.once('SIGTERM', () => pool.end()) return createMySQL2Executor(pool) }, reExportAdapterScript: `export { createMySQLAdapter as ${ADAPTER_FACTORY_FUNCTION_NAME} } from '/data/mysql-core/index.js';`, }, sqlserver: null, } export class Studio implements Command { private static help = format(` Browse your data with Prisma Studio ${bold('Usage')} ${dim('$')} prisma studio [options] ${bold('Options')} -h, --help Display this help message -p, --port Port to start Studio on -b, --browser Browser to open Studio in --config Custom path to your Prisma config file --url Database connection string (overrides the one in your Prisma config) ${bold('Examples')} Start Studio on the default port ${dim('$')} prisma studio Start Studio on a custom port ${dim('$')} prisma studio --port 5555 Start Studio in a specific browser ${dim('$')} prisma studio --port 5555 --browser firefox ${dim('$')} BROWSER=firefox prisma studio --port 5555 Start Studio without opening in a browser ${dim('$')} prisma studio --port 5555 --browser none ${dim('$')} BROWSER=none prisma studio --port 5555 Specify a custom prisma config file ${dim('$')} prisma studio --config=./prisma.config.ts Specify a direct database connection string ${dim('$')} prisma studio --url="postgresql://user:password@localhost:5432/dbname" `) static new(): Studio { return new Studio() } help(error?: string): string | HelpError { if (error) { return new HelpError(`\n${bold(red(`!`))} ${error}\n${Studio.help}`) } return Studio.help } /** * Parses arguments passed to this command, and starts Studio * * @param argv Array of all arguments * @param config The loaded Prisma config */ async parse(argv: string[], config: PrismaConfigInternal): Promise<string | Error> { const args = arg(argv, { '--help': Boolean, '-h': '--help', '--config': String, '--port': Number, '-p': '--port', '--browser': String, '-b': '--browser', '--url': String, }) if (isError(args)) { return this.help(args.message) } if (args['--help']) { return this.help() } const connectionString = args['--url'] || config.datasource?.url if (!connectionString) { return new Error( 'No database URL found. Provide it via the `--url <url>` argument or define it in your Prisma config file as `datasource.url`.', ) } if (!URL.canParse(connectionString)) { return new Error('The provided database URL is not valid.') } const protocol = new URL(connectionString).protocol.replace(':', '') const studioStuff = CONNECTION_STRING_PROTOCOL_TO_STUDIO_STUFF[protocol] if (!studioStuff) { return new Error(`Prisma Studio is not supported for the "${protocol}" protocol.`) } const executor = await studioStuff.createExecutor( connectionString, getUrlBasePath(args['--url'], config.loadedFromFile), ) const app = new Hono() app.use('*', cors()) app.get('/', (ctx) => { const contentType = FILE_EXTENSION_TO_CONTENT_TYPE[extname('index.html')] return ctx.text(INDEX_HTML, 200, { 'Content-Type': contentType }) }) app.get(`/${ADAPTER_FILE_NAME}`, (ctx) => { const contentType = FILE_EXTENSION_TO_CONTENT_TYPE[extname(ctx.req.path)] return ctx.text(studioStuff.reExportAdapterScript, 200, { 'Content-Type': contentType }) }) app.get('/*', async (ctx) => { const filePath = join(STATIC_ASSETS_DIR, ctx.req.path.substring(1)) const contentType = FILE_EXTENSION_TO_CONTENT_TYPE[extname(filePath)] || DEFAULT_CONTENT_TYPE try { return ctx.body(await readFile(filePath), 200, { 'Content-Type': contentType }) } catch { return ctx.text('Not Found', 404) } }) app.post('/bff', async (ctx) => { const request = (await ctx.req.json()) as StudioBFFRequest const { procedure } = request if (procedure === 'query') { const [error, results] = await executor.execute(request.query) if (error) { return ctx.json([serializeError(error)]) } return ctx.json([null, results]) } if (procedure === 'sequence') { if (!('executeSequence' in executor)) { return ctx.json([[serializeError(new Error('Executor does not support sequences'))]]) } const [[error0, result0], maybeResult1] = await (executor as SequenceExecutor).executeSequence(request.sequence) if (error0) { return ctx.json([[serializeError(error0)]]) } const [error1, result1] = maybeResult1 || [] if (error1) { return ctx.json([[null, result0], [serializeError(error1)]]) } return ctx.json([ [null, result0], [null, result1], ]) } procedure satisfies undefined return ctx.text('Unknown procedure', { status: 500 }) }) let projectHash: string | null = null const version = packageJson.dependencies['@prisma/studio-core-licensed'] app.post('/telemetry', async (ctx) => { const { eventId, name, payload, timestamp } = await ctx.req.json<Parameters<NonNullable<StudioProps['onEvent']>>[0]>() if (name !== 'studio_launched') { return ctx.body(null, 200) } const input: Check.Input = { check_if_update_available: false, client_event_id: eventId, command: name, information: JSON.stringify({ eventPayload: payload, protocol }), local_timestamp: timestamp, product: 'prisma-studio-cli', project_hash: (projectHash ??= digest(process.cwd())), version, } await sendEvent(input).catch(() => { // noop }) return ctx.body(null, 200) }) const port = args['--port'] || (await getPort({ port: DEFAULT_PORT, portRange: [MIN_PORT, DEFAULT_PORT - 1] })) const url = `http://localhost:${port}` const server = serve({ fetch: app.fetch, overrideGlobalObjects: false, port }, () => { process.once('SIGINT', () => server.close()) process.once('SIGTERM', () => server.close()) console.log(bold(`\nPrisma Studio is running at:`), url) const browser = args['--browser'] || process.env.BROWSER if (browser?.toLowerCase() !== 'none') { void open(url, { app: browser ? { name: browser } : undefined }) } }) return '' } } function getUrlBasePath(url: string | undefined, configPath: string | null): string { return url ? process.cwd() : configPath ? dirname(configPath) : process.cwd() } // prettier-ignore const INDEX_HTML = `<!doctype html> <html lang="en" style="height: 100%"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.1.17"></script> <link rel="stylesheet" href="/ui/index.css"> <style> body { color: black; height: 100%; margin: 0; padding: 0; } #root { height: 100%; } </style> <script type="importmap"> { "imports": { "react": "https://esm.sh/react@19.2.0", "react/jsx-runtime": "https://esm.sh/react@19.2.0/jsx-runtime", "react-dom": "https://esm.sh/react-dom@19.2.0", "react-dom/client": "https://esm.sh/react-dom@19.2.0/client" } } </script> </head> <body> <div id="root"></div> <script type="module"> 'use strict'; import React from 'react'; import ReactDOMClient from 'react-dom/client'; import { ${ADAPTER_FACTORY_FUNCTION_NAME} } from '/${ADAPTER_FILE_NAME}'; import { createStudioBFFClient } from '/data/bff/index.js'; import { Studio } from '/ui/index.js'; const adapter = ${ADAPTER_FACTORY_FUNCTION_NAME}({ executor: createStudioBFFClient({ url: '/bff' }), }); const onEvent = (event) => { fetch('/telemetry', { body: JSON.stringify(event), method: 'POST', }); }; window.__PVCE__ = true; const container = document.getElementById('root'); const root = ReactDOMClient.createRoot(container); root.render(React.createElement(Studio, { adapter, onEvent })); </script> </body> </html>`

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/prisma/prisma'

If you have feedback or need assistance with the MCP directory API, please join our Discord server