Nfs add mcp
Claude Code plugin: scaffold and maintain lightweight back-office apps on pure Next.js (App Router) — Server Components for reads, Server Actions for writes, services in src/server/modules/. No tRPC.
npx -y skills add juncoding/nextjs-fullstack-starter --skill nfs-add-mcpAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Add an MCP (Model Context Protocol) entry point to an existing project scaffolded with nextjs-fullstack-starter. Use when the user wants AI clients (Claude Desktop, Cursor) to query the project's data over OAuth, mentions MCP, wants to expose tools to AI, or invokes /nfs-add-mcp. Wires up /api/mcp/route.ts inside an (mcp) route group, the MCP plugin in Better Auth (the project's OAuth provider), the .well-known OAuth discovery endpoints, a tool registry at src/server/mcp/registry.ts, one example tool, and the migration for the three OAuth tables. Requires Better Auth to be wired — if not, prompts to run /nfs-add-auth first.
SKILL.md
8.5 KB, as published. Nobody here has run it
Add an MCP route to an existing scaffold
Use when a project from nextjs-fullstack-starter needs an MCP entry point so AI clients can call services over OAuth.
Pre-flight checks
- Refuse if Better Auth isn't wired. MCP uses Better Auth's
mcpplugin as its OAuth provider. Ifsrc/server/auth/index.tsdoesn't exist, tell the user to run/nfs-add-authfirst. - Refuse if
src/server/mcp/already exists. Suggest auditing what's there instead. - Refuse if
src/app/(mcp)/mcp/route.tsalready exists.
Plan
- Update
src/server/auth/index.tsto add themcpplugin alongsidenextCookies. - Add
src/app/(mcp)/mcp/route.ts— the POST handler that consumes the MCP transport, wrapped withwithMcpAuth. - Add
src/app/.well-known/oauth-authorization-server/route.ts+oauth-protected-resource/route.tsfor OAuth discovery. - Add
src/server/mcp/registry.ts— central tool registry. - Add
src/server/mcp/tools/_example.ts— one example tool that wraps the example service. - Add the three Better Auth OAuth tables (
OauthApplication,OauthAccessToken,OauthConsent) toschema.prisma. - Run
pnpm prisma migrate dev --name add_mcp_oauth. - Update
CLAUDE.md.
File templates
src/server/auth/index.ts (updated)
import "server-only";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js";
import { mcp } from "better-auth/plugins";
import { db } from "@/server/db/client";
import { env } from "@/env";
export const auth = betterAuth({
database: prismaAdapter(db, { provider: "postgresql" }),
baseURL: env.BETTER_AUTH_URL,
secret: env.BETTER_AUTH_SECRET,
emailAndPassword: { enabled: true, autoSignIn: true },
plugins: [
nextCookies(),
mcp({
loginPage: "/login",
}),
],
});
export type Session = typeof auth.$Infer.Session;
src/app/(mcp)/mcp/route.ts
import { withMcpAuth } from "better-auth/plugins";
import { auth } from "@/server/auth";
import { mcpRegistry } from "@/server/mcp/registry";
export const POST = withMcpAuth(auth, async (req, session) => {
return mcpRegistry.handle(req, { userId: session.user.id });
});
src/server/mcp/registry.ts
import "server-only";
import { exampleSearchTool } from "./tools/_example";
const TOOLS = {
example_search: exampleSearchTool,
} as const;
type ToolName = keyof typeof TOOLS;
export const mcpRegistry = {
list() {
return Object.entries(TOOLS).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
},
async handle(req: Request, ctx: { userId: string }) {
const body = await req.json();
if (body.method === "tools/list") {
return Response.json({ tools: this.list() });
}
if (body.method === "tools/call") {
const { name, arguments: args } = body.params;
const tool = TOOLS[name as ToolName];
if (!tool) {
return Response.json({ error: { code: -32601, message: `Unknown tool: ${name}` } });
}
const result = await tool.run(ctx.userId, args);
return Response.json({ content: [{ type: "text", text: JSON.stringify(result) }] });
}
return Response.json({ error: { code: -32601, message: "Unsupported method" } });
},
};
src/server/mcp/tools/_example.ts
import "server-only";
import { z } from "zod";
import { exampleService } from "@/server/modules/_example/_example.service";
const InputSchema = z.object({
q: z.string().optional(),
limit: z.number().int().min(1).max(50).optional(),
});
export const exampleSearchTool = {
description: "Search examples by query string. Returns up to `limit` results.",
inputSchema: {
type: "object",
properties: {
q: { type: "string", description: "Search query" },
limit: { type: "number", description: "Max results (default 20)" },
},
},
async run(userId: string, args: unknown) {
const input = InputSchema.parse(args);
return exampleService.list(userId, input);
},
};
The tool wraps the existing service. Permissions and audit are inherited unchanged — the service still calls requirePermission(userId, "example:read").
.well-known OAuth discovery routes
Better Auth's MCP plugin exposes the discovery metadata through helper handlers. The shape:
// src/app/.well-known/oauth-authorization-server/route.ts
import { auth } from "@/server/auth";
export const GET = async () => {
const metadata = await auth.api.getMcpDiscoveryMetadata();
return Response.json(metadata);
};
// src/app/.well-known/oauth-protected-resource/route.ts
import { auth } from "@/server/auth";
export const GET = async () => {
const metadata = await auth.api.getMcpProtectedResourceMetadata();
return Response.json(metadata);
};
If the Better Auth MCP API surface is different in your installed version, consult the upstream docs and adapt — the principle is "expose the metadata Better Auth provides."
Prisma — three OAuth tables
Add to schema.prisma:
model OauthApplication {
id String @id @default(cuid())
name String
clientId String @unique
clientSecret String?
redirectURLs String
type String
metadata String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OauthAccessToken {
id String @id @default(cuid())
accessToken String @unique
refreshToken String? @unique
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
clientId String
userId String
scopes String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([clientId])
}
model OauthConsent {
id String @id @default(cuid())
clientId String
userId String
scopes String
consentGiven Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([clientId, userId])
}
Migrate: pnpm prisma migrate dev --name add_mcp_oauth.
Update CLAUDE.md
Add a section:
## MCP
Read-only MCP endpoint at `POST /mcp`, gated by OAuth via Better Auth's `mcp` plugin.
- Tools live under `src/server/mcp/tools/`, registered in `src/server/mcp/registry.ts`.
- Each tool wraps an existing service method — permissions + audit inherited unchanged.
- Local dev caveat: Claude Desktop only accepts HTTPS URLs, so testing against `http://localhost` requires a tunnel (cloudflared, ngrok). Production / staging serve HTTPS natively.
To add a new tool:
1. Write the tool file under `src/server/mcp/tools/<name>.ts`. Wrap an existing service.
2. Register it in `src/server/mcp/registry.ts`.
3. No DB migration needed.
Verification
pnpm install
pnpm prisma generate
pnpm prisma migrate dev --name add_mcp_oauth
pnpm verify
Smoke test with a real MCP client:
- Run the app locally on HTTPS (tunnel if needed).
- In Claude Desktop, add the MCP server URL.
- Trigger the OAuth flow (login → consent → token).
- Ask Claude to call
example_search.
Anti-patterns to refuse
- Adding MCP tools that don't wrap services. Inline DB queries in tools break the permission/audit invariants. Always wrap a service.
- Tools that take userId from the args. The userId comes from
withMcpAuth's session, not the client. Don't trust client-supplied identity. - Tools without input validation. Use Zod on every tool's input — the MCP client may be a different model than the one you tested with.
- Mutating tools without explicit user confirmation. For Phase 1, ship read-only — every tool calls a
list/findById/searchservice method. Add write tools only after explicit safe-write design (per-tool dry-run mode, idempotency keys, an audit row tagged withsource: "mcp"so staff can review what an AI client did).