# About MCP Servers & How We Built One for Prisma URL: https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma Published: 2025-05-07 Authors: Nikolas Burk Tags: education, orm Description: Learn what MCP servers are and how Prisma built its MCP server, from tool access concepts to remote server architecture and practical implementation details. > Blog posts describe Prisma as it was on the date shown and are not kept current. Before implementing from a post, check the documentation index at https://www.prisma.io/docs/llms.txt and the changelog at https://www.prisma.io/changelog.md. > For an index of every published post, fetch https://www.prisma.io/blog/llms.txt. Learn how MCP works by following the practical example of how we built the Prisma MCP server, including the tradeoffs between local and remote MCP servers, the `@modelcontextprotocol/sdk` package, and how we enabled LLMs to use the Prisma CLI. ## Understanding MCP [#understanding-mcp] To deploy your own application tools, the [TypeScript MCP server hosting guide](https://www.prisma.io/compute/typescript-mcp-server-postgres) covers authenticated HTTP access, Postgres connections and testing the deployed service. Before diving into the technical details of how we built the Prisma MCP server, let's take a quick step back and understand MCP servers from the beginning. ### What if an LLM needs access to proprietary data or systems? [#what-if-an-llm-needs-access-to-proprietary-data-or-systems] LLMs are trained on information from the world wide web and can provide accurate answers to even the most esoteric questions that have been discussed somewhere on the internet. But what if you want your LLM to **answer questions based on proprietary data or systems**? Or to **perform some kind of action** on your behalf? Imagine prompting ChatGPT with the following: * "Find all the invoices from last year on my file system." * "Create a new database instance in the `us-west` region for me." * "Open a new GitHub issue in a specific repo." A purely internet-trained LLM won't be able to help with these because it doesn't have access to your file system, your database provider or the GitHub API. ### "Tool access" enables LLMs to interact with the outside world [#tool-access-enables-llms-to-interact-with-the-outside-world] In such scenarios, an LLM needs additional capabilities to interact with the "outside world"—to go beyond its knowledge of the web and perform actions on other systems. LLM providers have responded to this need by implementing so-called *tool* access. Individual providers use different names for it: OpenAI calls it [function calling](https://platform.openai.com/docs/guides/function-calling?api-mode=responses), Anthropic refers to it as [tool use](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview), and others use terms like "plugins" or "actions." ![](https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma/imgs/5a5b498ea72ab4a0969ed268408ef95d6f301d53-2888x1372.png) This approach was messy because each LLM had a different interface for interacting with tools. For example, if you wanted multiple LLMs to access your file system, you would need to implement the same "file system access tool" multiple times, each tailored to a specific LLM. Here's what that might have looked like: ```ts // OpenAI SDK import { OpenAI } from "openai"; const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); const response = await openai.chat.completions.create({ model: "gpt-4", messages: [{ role: "user", content: "Find all invoices from last year" }], functions: [{ name: "findInvoices", parameters: { year: { type: "number" } } }], }); if (response.function_call?.name === "findInvoices") { const { year } = JSON.parse(response.function_call.arguments); const files = fs.readdirSync("/invoices"); const matches = files.filter(f => f.includes(year)); const contents = matches.map(f => fs.readFileSync(`/invoices/${f}`, "utf-8")); } ``` ```ts // Anthropic SDK import { Anthropic } from "@anthropic-ai/sdk"; const anthropic = new Anthropic({ apiKey: ANTHROPIC_API_KEY }); const prompt = ` You may call find_invoices(year) to list PDFs from that year. User: Find all invoices from last year Assistant: `; const output = await anthropic.completions.create({ prompt }); if (output.includes("find_invoices")) { const year = extractYear(output); const files = fs.readdirSync("/invoices"); const matches = files.filter(f => f.includes(year)); const contents = matches.map(f => fs.readFileSync(`/invoices/${f}`, "utf-8")); } ``` With new LLMs popping up left and right, you can imagine how chaotic this will become if every LLM has its own, proprietary interface for accessing the outside world. ### Introducing MCP: Standardizing tool access for LLMs [#introducing-mcp-standardizing-tool-access-for-llms] In November 2024, [Anthropic introduced the Model Context Protocol](https://www.anthropic.com/news/model-context-protocol) (MCP) as a: > new **standard for connecting AI assistants to the systems where data lives**, including content repositories, business tools, and development environments. MCP provides a universal, open standard for connecting AI systems with external data sources. All LLMs that implement the MCP protocol can now access the same functionality if it's exposed through an MCP server. ![](https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma/imgs/cbb0879b85e332bad731a1ca4c514d7c66ea9616-2888x1372.png) Returning to the previous example: With MCP, you only need to implement the invoice search functionality *once*. You can then expose it via an MCP server to all LLMs that support the MCP protocol. Here's a pseudocode implementation: ```ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new MCPServer(); server.tool({ name: "findInvoices", parameters: { year: "number" }, handler: ({ year }) => { const files = fs.readdirSync("/invoices"); const matches = files.filter(f => f.includes(year)); return matches.map(f => fs.readFileSync(`/invoices/${f}`, "utf-8")); }, }); const transport = new StdioServerTransport(); await server.connect(transport); ``` Anthropic clearly struck a chord with this standard. If you were on X at the time, you probably saw multiple MCP posts a day. Google Trends for "MCP" tell the same story: ![](https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma/imgs/03cc8051d3cdfcabcb6037fcb5fbe1d33856fc99-1816x1400.png) ### How to connect an LLM to an MCP server [#how-to-connect-an-llm-to-an-mcp-server] All you need to augment an LLM with MCP server functionality is a CLI command to launch the server. Most AI tools accept a JSON configuration like this: ```js { "mcpServers": { "weather": { "command": "uv", "args": ["--directory", "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather", "run", "weather.py"] } } } ``` The AI tool runs the `command`, passes the `args`, and the LLM gains access to the server's tools. ## Building the Prisma MCP server [#building-the-prisma-mcp-server] At Prisma, we've built the [most popular TypeScript ORM](https://www.prisma.io/blog/how-prisma-orm-became-the-most-downloaded-orm-for-node-js) and the [world's most efficient Postgres database](https://www.prisma.io/blog/announcing-prisma-postgres-early-access) running on unikernels. Naturally, we wondered how we could use MCP to simplify database workflows for developers. ### Why build an MCP server for Prisma? [#why-build-an-mcp-server-for-prisma] Many developers use AI coding tools like [Cursor](https://www.cursor.com/) or [Windsurf](https://windsurf.com/) when building data-driven apps with Prisma. These AI coding tools have so-called *agent modes* where the AI edits source files for you, and you simply need to *review* and *accept* the suggestions made by the AI. It can also offer to run a CLI command for you, and like with file edits, you need to *confirm* that this command should actually be executed. Since many interactions with Prisma Postgres and Prisma ORM are driven by the Prisma CLI, we wanted to make it possible for an LLM to run Prisma CLI commands on your behalf, e.g. for these workflows: * Checking the status of database migrations * Creating and running database migrations * Authenticating with the [Prisma Console](https://console.prisma.io/) * Provisioning new Prisma Postgres instances Before MCP, we would have had to implement support separately for each LLM. With MCP, we can implement a single server that supports all of them at once. ### The `@modelcontextprotocol/sdk` package: "Like Express for MCP" [#the-modelcontextprotocolsdk-package-like-express-for-mcp] When introducing MCP, Anthropic released SDKs for various languages. The TypeScript SDK lives in the [`typescript-sdk`](https://github.com/modelcontextprotocol/typescript-sdk) repository and provides everything needed to implement MCP clients and servers. ![](https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma/imgs/e7b3db57367968f0458172fb2f8bfa532b82f7ed-2068x1600.png) ### Local vs. remote MCP servers [#local-vs-remote-mcp-servers] When building an MCP server, you must decide if it runs *locally* (on the same machine as the user) or *remotely* (on a machine accessible via the internet). This depends on what the server does. If it needs access to the user's file system, it must run locally. If it just calls APIs, it can be local or remote (because an API can be called both from a local and remote machine). ![](https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma/imgs/39eadb7214d14e787cf74adbcbd9940f6cdb4ae2-2888x2644.png) In the case of Prisma, the LLM primarily needs access to the Prisma CLI in order to support developers with database-related workflows. The Prisma CLI may connect to a local or a remote database instance. However, because the CLI commands are executed locally, the Prisma MCP server also must be local. ### Enabling LLMs to call Prisma CLI commands [#enabling-llms-to-call-prisma-cli-commands] The Prisma MCP server is quite simple and lightweight—[you can explore it on GitHub](https://github.com/prisma/orm/blob/main/packages/cli/src/mcp/MCP.ts). It has been packaged as part of the Prisma CLI and can be started with the following command: ```shell prisma mcp ``` Here's its basic structure: ```ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import type { PrismaConfigInternal } from '@prisma/config' import { Command } from '@prisma/internals' import execa from 'execa' async function runCommand({ args, cwd }: { args: string[]; cwd: string }) { const result = await execa.node(process.argv[1], args, { cwd }) return `${result.stdout}\n${result.stderr}` } export class Mcp implements Command { public static new(): Mcp { return new Mcp() } public async parse(_argv: string[], _config: PrismaConfigInternal): Promise { const server = new McpServer({ name: 'Prisma', version, }) /** * The capabilities of the MCP are implemented here via * the `server.tool(...)` API. */ const transport = new StdioServerTransport() await server.connect(transport) return '' } } ``` The `parse` function is being executed when the `prisma mcp --early-access` CLI command is invoked. It starts an MCP server that uses the `StdioServerTransport` (as opposed to `StreamableHTTPServerTransport`) because it’s running locally. What’s not shown in the above snippet is the actual implementation of the CLI commands, let’s zoom into the `parse` function and look at the `prisma migrate dev` and `prisma init --db` commands as examples: ```ts public async parse(_argv: string[], _config: PrismaConfigInternal): Promise { const server = new McpServer({ name: 'Prisma', version, }) server.tool( 'migrate-dev', `Prisma Migrate Dev is used to update Prisma whenever the schema.prisma file has been modified. Always provide a descriptive name argument describing the change that was made to the Prisma Schema. The migrate dev command performs these steps: 1. Reruns the existing migration history in the shadow database in order to detect schema drift (edited or deleted migration file, or a manual changes to the database schema) 2. Applies pending migrations to the shadow database (for example, new migrations created by colleagues) 3. Generates a new migration from any changes you made to the Prisma schema before running migrate dev 4. Applies all unapplied migrations to the development database and updates the _prisma_migrations table 5. Triggers the generation of artifacts (for example, Prisma Client)`, { name: z.string(), projectCWD: z.string() }, async ({ name, projectCWD }) => { const text = await runCommand({ cwd: projectCWD, args: ['migrate', 'dev', '--name', name] }) return { content: [{ type: 'text', text }] } }, ) server.tool( 'Create-Prisma-Postgres-Database', `Create a new online Prisma Postgres database. Specify a name that makes sense to the user - maybe the name of the project they are working on. Specify a region that makes sense for the user. Pick between these three options: us-east-1, eu-west-3, ap-northeast-1. If you are unsure, pick us-east-1. Provide the current working directory of the users project. This should be the top level directory of the project. If the response idicates that you have reached the workspace plan limit, you should instruct the user to do one of these things: - If they want to connect to an existing database, they should go to console.prisma.io and copy the connection string - If they want to upgrade their plan, they should go to console.prisma.io and upgrade their plan in order to be able to create more databases - If they want to delete a database they no longer need, they should go to console.prisma.io and delete the database project`, { name: z.string(), region: z.string(), projectCWD: z.string() }, async ({ name, region, projectCWD }) => { const text = await runCommand({ cwd: projectCWD, args: ['init', '--db', '--name', name, '--region', region, '--non-interactive'], }) return { content: [{ type: 'text', text }] } }, ) const transport = new StdioServerTransport() await server.connect(transport) return '' } ``` Each tool is registered via `server.tool()` with: 1. A **name** (so the LLM can reference it) 2. A **description** (to help the LLM understand its purpose) 3. An **argument schema** (we use [`zod`](https://zod.dev/)) 4. A **function** implementing the logic Our implementation of all tools follows the same pattern and is quite straightforward: When a tool is invoked, we simply spawn a new process (via the `runCommand` function which uses `execa`) to execute the CLI command that belongs to it. That's all you need to enable an LLM to invoke commands on behalf of users. ## Try the Prisma MCP server now [#try-the-prisma-mcp-server-now] If you're curious to try it, paste this snippet into the MCP config section of your favorite AI tool: ```js { "mcpServers": { "Prisma": { "command": "npx", "args": ["-y", "prisma", "mcp"] } } } ``` Or check out our [docs](https://www.prisma.io/docs/ai/tools/mcp-server) for specific instructions for Cursor, Windsurf, Claude, or the OpenAI Agents SDK. Once added, your AI tool will show the MCP server's status and available tools. Here's how it looks in Cursor: ![](https://www.prisma.io/blog/about-mcp-servers-and-how-we-built-one-for-prisma/imgs/1a9c595ed52f76150075536f39a1d00a014da6b1-1950x1240.png) ## Beyond MCP: New capabilities for Prisma users in VS Code [#beyond-mcp-new-capabilities-for-prisma-users-in-vs-code] What's next? While MCP is powerful, it still requires manual setup in AI tools. Our goal is to assist developers *where they already are*. VS Code is the de-facto standard for building web apps, and thanks to its free integration of GitHub Copilot and the [LanguageModelTool API](https://code.visualstudio.com/api/extension-guides/tools), we’re going to bring the capabilities of the MCP server to all Prisma VS Code extension users 🎉. This means Copilot will be capable of assisting you even more with your database workflows very soon! ## Share your feedback with us [#share-your-feedback-with-us] Have thoughts or questions about MCP, AI tools, or Prisma in general? [Reach out on X](https://pris.ly/x) or [join our community on Discord](https://pris.ly/discord) — we'd love to hear from you!