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