Nextjs 16, Prisma 7 and Better Auth

Setting up Nextjs

This will install nextjs in the current directory

npx create-next-app@latest . 

Install Prisma v7

npm install prisma tsx @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
npx prisma init --output ../app/generated/prisma

In the .env file add the following line

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/tasks?schema=public

Create Prisma Client

Create lib/prisma.ts and paste the following

import { PrismaClient } from "../app/generated/prisma/client"; 
import { PrismaPg } from "@prisma/adapter-pg"; 
const globalForPrisma = global as unknown as {
  prisma: PrismaClient; 
}; 
const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL, 
}); 
const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    adapter, 
  }); 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 
export default prisma; 

Generate the prisma client

npx prisma generate

Install Better Auth

npm install better-auth

In .env add the following

BETTER_AUTH_SECRET=my-secret # replace the secret
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app

Create a Better Auth Instance

Create lib/auth.ts and paste the following

import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import prisma from '@/lib/prisma'
import { nextCookies } from 'better-auth/next-js'

export const auth = betterAuth({
    database: prismaAdapter(prisma, {
        provider: 'postgresql',
    }),
    emailAndPassword: {
        enabled: true,
    },
    plugins: [nextCookies()]
})

Generate the database schema

npx auth@latest generate --config lib/auth.ts

This will generate all the models in prisma/shcema.prisma. Now add the following at the end of the file

model Task {
  id        String   @id @default(cuid())
  title     String
  completed Boolean  @default(false)
  priority  String   @default("medium")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
}

Also update the Model User that was created by generate

task         Task[]

Now run the prisma migration

npx prisma migrate dev --name init

Install Shadcn Login and Sign Up form

npx shadcn@latest add signup-01
npx shadcn@latest add login-01

This will create two new folders login and singup under the app folder

Create (auth) folder

Create the auth folder and move the login and signup folders under that

app --> (auth) --> login --> page.tsx
app --> (auth) --> signup --> page.tsx

Create actions.ts file

Create an lib --> actions.ts

"use server"
import { auth } from "@/lib/auth"
import { redirect } from "next/navigation";

export async function signUpUser(formData: FormData) {
    const name = formData.get("name") as string;
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;

    await auth.api.signUpEmail({
        body: {
            name,
            email,
            password,
        }
    })
    redirect('/');
}

export async function signInUser(formData: FormData) {
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;

    await auth.api.signInEmail({
        body: {
            email,
            password,
        },
    })
    redirect('/');
}

Modify signup-form.tsx

Add form action

<form action={signUpUser}>

Add name keys to all the form fields

<Input name="name" id="name" type="text" placeholder="John Doe" required />
<Input name="email" id="email" type="email" placeholder="[email protected]" required />
<Input name="password" id="password" type="password" required />

Modify login-form.tsx

Add form action

<form action={signInUser}>

Add name keys to all the form fields

<Input name="email" id="email" type="email" placeholder="[email protected]" required />
<Input name="password" id="password" type="password" required />

Server session for Auth

Create file lib --> getServerSession.ts. The goal is to get the user. If there is no server session we will not allow the user to login

import { headers } from "next/headers";
import { auth } from "@/lib/auth";

export async function getServerSession() {
    const session = await auth.api.getSession({ headers: await headers() });
    return session;
}

Check login user for access

Create a folder app --> (main) and move the page.tsx file from app to this folder
Update page.tsx to check for session

import { getServerSession } from "@/lib/getServerSession";
import { redirect } from "next/navigation";

export default async function Home() {
  const session = await getServerSession()
  console.log(session)
  if (!session) {
    redirect("/login")
  } else
  return (
    <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
      <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
        Hello {session.user.name}
      </main>
    </div>
  );
}

You can now sign up and login. Once you login you should be able to go to the main landing page and be greeted as Hello $yourName

Add Tasks

Lets install the card components from shadcn

npx shadcn@latest add card

Add a create task in actions.ts

export async function addTask(formData: FormData) {
    const session = await getServerSession();
    if (!session?.user) throw new Error("Unauthorized");

    const data = {
        task_name: formData.get("title"),
        task_completed: formData.get("completed") == "true",
    };

    const task = await prisma.task.create({
        data: {
            title: data.task_name as string,
            completed: data.task_completed as boolean,
            userId: session.user.id,
        },
    });

    // revalidatePath('/')
    redirect('/');
}

Create components --> addTask.tsx

Taken from the Card component in shadcn and modified

import { Button } from "@/components/ui/button"
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { addTask } from "@/lib/actions"

export function AddTask() {
  return (
    <Card className="w-full max-w-sm">
      <CardHeader>
        <CardTitle>Add Task</CardTitle>
        <CardDescription>
          Add your tasks
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form action={addTask}>
          <div className="flex flex-col gap-6">
            <div className="grid gap-2">
              <Label htmlFor="title">Title</Label>
              <Input
                name="title"
                id="title"
                type="text"
                placeholder="Set up a meeting with John"
                required
              />
            </div>
            <div className="grid gap-2">
              <div className="flex items-center">
                <Label htmlFor="completed">Completed</Label>
              </div>
              <Input name="completed" id="completed" type="boolean" required />
            </div>

            <div className="grid gap-2">
              <div className="flex items-center">
                <Button type="submit" className="w-full">
                    Submit
                </Button>
            </div>
          </div>
            </div>
        </form>
      </CardContent>
    </Card>
  )
}

Create app --> (main) --> addtask --> page.tsx

import { AddTask } from "@/components/addTask";
import { getServerSession } from "@/lib/getServerSession";
import { redirect } from "next/navigation";
export default async function addTask() {
  const session = await getServerSession();
  if (!session) {
    redirect("/login")
  } else 
  return (
    <div className="min-h-screen flex items-center justify-center">
        <AddTask />
    </div>
  );
}

List Tasks

Install dependencies

npm install lucide-react
npx shadcn@latest add badge
npx shadcn@latest add table

Get all the tasks

Add the following in actions.ts

export async function getTasks() {
    const session = await getServerSession();
    if (!session?.user) throw new Error("Unauthorized");
    try {
        const tasks = await prisma.task.findMany({
            where: {
                userId: session.user.id,
                },
            orderBy: {
                createdAt: "desc",
                },
        })

        return tasks
    } catch (error) {
        console.error("Error fetching tasks:", error)
        throw new Error("Failed to fetch tasks")
        }
    }

Update the homepage to list all tasks

Update app --> (main) --> page.tsx

import { getServerSession } from "@/lib/getServerSession";
import { redirect } from "next/navigation";
import { getTasks } from "@/lib/actions";
import { Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { EditTask } from "@/components/editTask";
import { DeleteTask } from "@/components/deleteTask";
import { Badge } from "@/components/ui/badge"
import Link from "next/link";
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"



export default async function Home() {
  const session = await getServerSession();
  if (!session) {
    redirect("/login")
  }
  const tasks = await getTasks();

  return (
    <div className="min-h-screen bg-muted/30 p-4 md:p-6">
      {/* Header */}
      <div className="mb-6 flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-semibold tracking-tight">Tasks</h1>
          <p className="text-sm text-muted-foreground">
            Manage and track your work
          </p>
        </div>
        <Link href="/addtask">
          <Button className="gap-2">
            <Plus className="h-4 w-4" />
            Add Task
          </Button>
        </Link>
      </div>

      {/* Task Table */}
      <div>
        <Table>
          <TableCaption>All your tasks</TableCaption>
          <TableHeader>
            <TableRow>
              <TableHead className="w-[400px]">Task Title</TableHead>
              <TableHead>Priority</TableHead>
              <TableHead>Status</TableHead>
              <TableHead className="text-right">Actions</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {tasks.map((task) => (
              <TableRow key={task.id}>
                <TableCell className="font-medium">{task.title}</TableCell>
                <TableCell>{task.priority}</TableCell>
                <TableCell>
                  <div className="flex shrink-0 items-center gap-2">
                    <Badge variant={task.completed ? "secondary" : "default"}>
                      {task.completed ? "Done" : "In progress"}
                    </Badge>
                  </div>
                </TableCell>
                <TableCell className="flex justify-end gap-3">
                  <EditTask task={task}/>
                  <DeleteTask task={task}/>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>  
      </div>
    </div>
  );
}

Edit Task

We will use the shadcn Dialog to component to edit

npx shadcn@latest add dialog

Create a Edit Task Dialog component

/app/components/editTask.tsx

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Pencil } from "lucide-react"
import { updateTask } from "@/lib/actions"

export function EditTask({ task }) {
  return (
    <Dialog>
        <DialogTrigger asChild>
          <Pencil className="w-4 h-4 cursor-pointer" />
        </DialogTrigger>
        <DialogContent className="sm:max-w-sm">
          <DialogHeader>
            <DialogTitle>Edit task</DialogTitle>
            <DialogDescription>
              Make changes to your task  here. Click save when done.
            </DialogDescription>
          </DialogHeader>
          <form action={updateTask}>
          <FieldGroup>
            <Field>
              <Label htmlFor="title">Title</Label>
              <Input id="title" name="title" defaultValue={task.title} />
            </Field>
            <Field>
              <Label htmlFor="completed">Username</Label>
              <Input id="completed" name="completed" defaultValue={task.completed} />
              <Input type="hidden" name="id" value={task.id} />
            </Field>
          </FieldGroup>
          <DialogFooter>
            <DialogClose asChild>
              <Button variant="outline">Cancel</Button>
            </DialogClose>
            <DialogClose asChild>
                <Button type="submit">Save changes</Button>
            </DialogClose>

          </DialogFooter>
          </form>
        </DialogContent>
    </Dialog>
  )
}

Add the Edit Action

Add the following in action.ts

export async function updateTask(formData: FormData) {
    const session = await getServerSession();
    if (!session?.user) throw new Error("Unauthorized");
    const data = {
        task_name: formData.get("title"),
        task_completed: formData.get("completed") == "true",
        task_id: formData.get("id") as string,
    };

    const task = await prisma.task.update({
        where: { 
            id: data.task_id,
            userId: session.user.id,
        },
        data: {
            title: data.task_name as string,
            completed: data.task_completed as boolean,
        },
    });

    revalidatePath('/')
}

Delete a Task

Add app/components/deleteTask.tsx

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Trash2 } from "lucide-react"
import { deleteTask } from "@/lib/actions"

export function DeleteTask({ task }) {
  return (
    <Dialog>
        <DialogTrigger asChild>
          <Trash2 className="w-4 h-4 cursor-pointer text-red-500" />
        </DialogTrigger>
        <DialogContent className="sm:max-w-sm">
          <DialogHeader>
            <DialogTitle>Delete task</DialogTitle>
            <DialogDescription>
              Delete the task
            </DialogDescription>
          </DialogHeader>
          <form action={deleteTask}>
          <FieldGroup>
            Are you sure you want to delete the task
            <Field>
              <Input type="hidden" name="id" value={task.id} />
            </Field>
          </FieldGroup>
          <DialogFooter>
            <DialogClose asChild>
                <Button type="submit">Delete</Button>
            </DialogClose>

          </DialogFooter>
          </form>
        </DialogContent>
    </Dialog>
  )
}

Add the Delete Action

Add the following in action.ts

export async function deleteTask(formData: FormData) {
    const session = await getServerSession();
    if (!session?.user) throw new Error("Unauthorized");

    await prisma.task.delete({
        where: {
            id: formData.get("id") as string,
            userId: session.user.id,
        },
    });

    revalidatePath('/');
}

Setup up API Keys

Add API Key model

in schema.prisma add

model User {
  id            String    @id
  name          String
  email         String
  emailVerified Boolean   @default(false)
  image         String?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  sessions      Session[]
  accounts      Account[]
  task          Task[]
  apiKeys       ApiKey[]  // add this
  @@unique([email])
  @@map("user")
}

model ApiKey {
  id        String    @id @default(cuid())
  key       String    @unique  // stores hashed key
  name      String
  userId    String
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt DateTime  @default(now())
  expiresAt DateTime?
  lastUsed  DateTime?

  @@index([userId])
  @@map("api_key")
}

Run the Migration

npx prisma migrate dev --name add_api_keys
npx prisma generate

Server Actions for API Key

Create the following lib/actions/apiKeys.ts

// lib/actions/apiKeys.ts
"use server"

import { getServerSession } from "@/lib/getServerSession"
import prisma from "@/lib/prisma"
import { revalidatePath } from "next/cache"
import { randomBytes, createHash } from "crypto"

function generateKey(): string {
    return `task_${randomBytes(32).toString("hex")}`
}

function hashKey(key: string): string {
    return createHash("sha256").update(key).digest("hex")
}

export async function createApiKey(formData: FormData) {
    const session = await getServerSession()
    if (!session?.user) throw new Error("Unauthorized")

    const name = formData.get("name") as string
    if (!name) throw new Error("Name is required")

    const rawKey = generateKey()
    const hashedKey = hashKey(rawKey)

    await prisma.apiKey.create({
        data: {
            key: hashedKey,
            name,
            userId: session.user.id,
        },
    })

    revalidatePath("/settings/api-keys")

    // Return raw key — this is the only time it's available
    return { rawKey }
}

export async function deleteApiKey(formData: FormData) {
    const session = await getServerSession()
    if (!session?.user) throw new Error("Unauthorized")

    const id = formData.get("id") as string

    await prisma.apiKey.delete({
        where: {
            id,
            userId: session.user.id,
        },
    })

    revalidatePath("/settings/api-keys")
}

export async function getApiKeys() {
    const session = await getServerSession()
    if (!session?.user) throw new Error("Unauthorized")

    return prisma.apiKey.findMany({
        where: { userId: session.user.id },
        select: {
            id: true,
            name: true,
            createdAt: true,
            lastUsed: true,
            expiresAt: true,
            // key is intentionally excluded
        },
        orderBy: { createdAt: "desc" },
    })
}

Create the API UI to generate keys

Create app/(main)/settings/api-keys/ApiKeysClient.tsx

// app/settings/api-keys/ApiKeysClient.tsx
"use client"

import { useState } from "react"
import { createApiKey, deleteApiKey } from "@/lib/apiKeys"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Copy, Trash2, Plus, Key } from "lucide-react"
import { getApiKeys } from "@/lib/apiKeys"

type ApiKey = {
  id: string
  name: string
  createdAt: Date
  lastUsed: Date | null
  expiresAt: Date | null
}

export function ApiKeysClient({ initialKeys }: { initialKeys: ApiKey[] }) {
  const [keys, setKeys] = useState(initialKeys)
  const [newKeyName, setNewKeyName] = useState("")
  const [revealedKey, setRevealedKey] = useState<string | null>(null)
  const [copied, setCopied] = useState(false)
  const [loading, setLoading] = useState(false)

  async function handleCreate() {
    if (!newKeyName.trim()) return
    setLoading(true)

    const formData = new FormData()
    formData.append("name", newKeyName)

    const result = await createApiKey(formData)
    setRevealedKey(result.rawKey)
    setNewKeyName("")

    // Fetch real keys from server instead of using a fake entry
    const updated = await getApiKeys()
    setKeys(updated)
    
    setLoading(false)
}

  async function handleDelete(id: string) {
    const formData = new FormData()
    formData.append("id", id)
    await deleteApiKey(formData)
    setKeys(prev => prev.filter(k => k.id !== id))
  }

  async function handleCopy() {
    if (!revealedKey) return
    await navigator.clipboard.writeText(revealedKey)
    setCopied(true)
    setTimeout(() => setCopied(false), 2000)
  }

  return (
    <div className="min-h-screen bg-muted/30 p-4 md:p-6 max-w-2xl mx-auto">
      <div className="mb-6">
        <h1 className="text-2xl font-semibold tracking-tight">API Keys</h1>
        <p className="text-sm text-muted-foreground">
          Manage API keys for external access via MCP
        </p>
      </div>

      {/* Revealed key banner */}
      {revealedKey && (
        <Card className="mb-6 border-green-200 bg-green-50">
          <CardHeader className="pb-2">
            <CardTitle className="text-sm text-green-800">
              API Key Created — Copy it now, it won't be shown again
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className="flex items-center gap-2">
              <code className="flex-1 rounded bg-white p-2 text-xs break-all border">
                {revealedKey}
              </code>
              <Button size="sm" variant="outline" onClick={handleCopy}>
                <Copy className="h-4 w-4" />
                {copied ? "Copied!" : "Copy"}
              </Button>
            </div>
            <Button
              size="sm"
              variant="ghost"
              className="mt-2 text-xs text-muted-foreground"
              onClick={() => setRevealedKey(null)}
            >
              Dismiss
            </Button>
          </CardContent>
        </Card>
      )}

      {/* Create new key */}
      <Card className="mb-6">
        <CardHeader>
          <CardTitle className="text-base">Create New Key</CardTitle>
          <CardDescription>Give it a name so you know what it's for</CardDescription>
        </CardHeader>
        <CardContent>
          <div className="flex gap-2">
            <div className="flex-1">
              <Label htmlFor="keyName" className="sr-only">Key name</Label>
              <Input
                id="keyName"
                placeholder="e.g. OpenWebUI"
                value={newKeyName}
                onChange={e => setNewKeyName(e.target.value)}
                onKeyDown={e => e.key === "Enter" && handleCreate()}
              />
            </div>
            <Button onClick={handleCreate} disabled={loading || !newKeyName.trim()} className="gap-2">
              <Plus className="h-4 w-4" />
              Generate
            </Button>
          </div>
        </CardContent>
      </Card>

      {/* Existing keys */}
      <Card>
        <CardHeader>
          <CardTitle className="text-base">Existing Keys</CardTitle>
        </CardHeader>
        <CardContent>
          {keys.length === 0 ? (
            <div className="flex flex-col items-center gap-2 py-8 text-muted-foreground">
              <Key className="h-8 w-8" />
              <p className="text-sm">No API keys yet</p>
            </div>
          ) : (
            <div className="flex flex-col gap-3">
              {keys.map(k => (
                <div key={k.id} className="flex items-center justify-between rounded-lg border p-3">
                  <div className="flex flex-col gap-1">
                    <span className="text-sm font-medium">{k.name}</span>
                    <div className="flex gap-2 text-xs text-muted-foreground">
                      <span>Created {new Date(k.createdAt).toLocaleDateString()}</span>
                      {k.lastUsed && (
                        <span>· Last used {new Date(k.lastUsed).toLocaleDateString()}</span>
                      )}
                    </div>
                  </div>
                  <div className="flex items-center gap-2">
                    <Badge variant="outline" className="text-xs">Active</Badge>
                    <Button
                      size="sm"
                      variant="ghost"
                      className="text-destructive hover:text-destructive"
                      onClick={() => handleDelete(k.id)}
                    >
                      <Trash2 className="h-4 w-4" />
                    </Button>
                  </div>
                </div>
              ))}
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  )
}

Cretae the page.tsx for API UI

Create app/(main)/settings/api-keys/page.tsx

// app/settings/api-keys/page.tsx
import { getApiKeys } from "@/lib/apiKeys"
import { ApiKeysClient } from "./ApiKeysClient"

export default async function ApiKeysPage() {
  const apiKeys = await getApiKeys()
  return <ApiKeysClient initialKeys={apiKeys} />
}

Setup MCP Server

Create a new folder outside of the Nextjs app

mkdir task_mcp
cd task_mcp/
mkdir prisma

Copy the prisma.schema from the Nextjs app

cp ../crm4/prisma/schema.prisma prisma/schema.prisma

Add a .env file

Grab this from the Nextjs app. It should be the same database

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/tasks1?schema=public

Initialize the app

npm init -y
npm install @modelcontextprotocol/sdk prisma @prisma/adapter-pg @prisma/client
npm install zod dotenv
npm install -D tsx typescript @types/node
npx tsc --init
npx prisma generate

Update package.json

{
  "name": "task_mcp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "tsx watch app/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "db:generate": "prisma generate"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "@prisma/adapter-pg": "^7.8.0",
    "@prisma/client": "^7.8.0",
    "dotenv": "^17.4.2",
    "prisma": "^7.8.0",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@types/node": "^25.9.1",
    "tsx": "^4.22.3",
    "typescript": "^6.0.3"
  }
}

Create app/index.ts

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { PrismaClient } from "./generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";
import { createHash } from "crypto";
import "dotenv/config";
import { z } from "zod";
import http from "http";

// --- Prisma setup ---
const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL,
});
const prisma = new PrismaClient({ adapter });

// --- Auth helper ---
async function getUserFromApiKey(authHeader: string | undefined): Promise<string> {
  if (!authHeader?.startsWith("Bearer ")) throw new Error("Missing or invalid Authorization header");

  const rawKey = authHeader.slice(7);
  const hashedKey = createHash("sha256").update(rawKey).digest("hex");

  const apiKey = await prisma.apiKey.findUnique({
    where: { key: hashedKey },
    include: { user: true },
  });

  if (!apiKey) throw new Error("Invalid API key");
  if (apiKey.expiresAt && apiKey.expiresAt < new Date()) throw new Error("API key expired");

  // Update lastUsed
  await prisma.apiKey.update({
    where: { id: apiKey.id },
    data: { lastUsed: new Date() },
  });

  return apiKey.userId;
}

// --- MCP Server factory ---
// We create a new McpServer per request so each request is isolated
function createMcpServer(userId: string): McpServer {
  const server = new McpServer({
    name: "task-mcp",
    version: "1.0.0",
  });

  server.tool(
    "get_tasks",
    "Get all tasks for the authenticated user",
    {},
    async () => {
      const tasks = await prisma.task.findMany({
        where: { userId },
        orderBy: { createdAt: "desc" },
      });
      return {
        content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
      };
    }
  );

  server.tool(
    "add_task",
    "Create a new task",
    {
      title: z.string().min(1).describe("Task title"),
      completed: z.boolean().optional().default(false).describe("Is the task completed?"),
      priority: z.enum(["low", "medium", "high"]).optional().default("medium").describe("Task priority"),
    },
    async ({ title, completed, priority }) => {
      const task = await prisma.task.create({
        data: { title, completed, priority, userId },
      });
      return {
        content: [{ type: "text", text: JSON.stringify(task, null, 2) }],
      };
    }
  );

  server.tool(
    "update_task",
    "Update an existing task",
    {
      id: z.string().describe("Task ID"),
      title: z.string().optional().describe("New title"),
      completed: z.boolean().optional().describe("Mark as completed or not"),
      priority: z.enum(["low", "medium", "high"]).optional().describe("New priority"),
    },
    async ({ id, title, completed, priority }) => {
      const task = await prisma.task.update({
        where: { id, userId },
        data: {
          ...(title !== undefined && { title }),
          ...(completed !== undefined && { completed }),
          ...(priority !== undefined && { priority }),
        },
      });
      return {
        content: [{ type: "text", text: JSON.stringify(task, null, 2) }],
      };
    }
  );

  server.tool(
    "delete_task",
    "Delete a task",
    {
      id: z.string().describe("Task ID to delete"),
    },
    async ({ id }) => {
      await prisma.task.delete({
        where: { id, userId },
      });
      return {
        content: [{ type: "text", text: `Task ${id} deleted successfully` }],
      };
    }
  );

  return server;
}

// --- HTTP Server ---
const PORT = process.env.PORT || 3001;

const httpServer = http.createServer(async (req, res) => {
  if (req.url !== "/mcp") {
    res.writeHead(404).end("Not found");
    return;
  }

  try {
    const userId = await getUserFromApiKey(req.headers["authorization"]);

    const server = createMcpServer(userId);
    const transport = new StreamableHTTPServerTransport({});

    res.on("close", () => {
      transport.close();
      server.close();
    });

    await server.connect(transport as any);
    await transport.handleRequest(req, res);
  } catch (err: any) {
    if (!res.headersSent) {
      res.writeHead(401, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: err.message }));
    }
  }
});

httpServer.listen(PORT, () => {
  console.log(`MCP server running on http://localhost:${PORT}/mcp`);
});

Add gitignore

Create a .gitignore

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

/app/generated/prisma