Base
MCP Server development framework with OAuth 2.1, Entity-Driven CRUD, and Streamable HTTP transport
Ask AI about Base
Powered by Claude Β· Grounded in docs
I know everything about Base. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
@nocoo/base-mcp
MCP Server development framework with OAuth 2.1, Entity-Driven CRUD, and Streamable HTTP transport support.
Features
- Entity-Driven CRUD Framework: Declarative entity definitions β automatic MCP tool generation
- OAuth 2.1 Support: PKCE, Dynamic Client Registration, token management
- DNS Rebinding Protection: Origin validation for HTTP transport
- Testing Utilities: Mock context, result parsing, token store mocking
Installation
pnpm add @nocoo/base-mcp
Peer dependencies:
pnpm add @modelcontextprotocol/sdk zod
Full Integration Guide
This guide covers the complete setup for integrating MCP with OAuth 2.1 authentication in a Next.js/Hono app.
Architecture Overview
βββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
β MCP Client β β Your App β
β (Claude Code, β β βββββββββββββββββββββββββββββββββββ β
β Cursor, etc.) ββββββΆβ β /.well-known/oauth-auth-server β β OAuth Discovery
β β β βββββββββββββββββββββββββββββββββββ β
β β β β
β ββββββΆβ /api/mcp/register (Client Reg) β
β ββββββΆβ /api/mcp/authorize (Auth Start) β
β ββββββΆβ /api/mcp/callback (Auth Complete) β
β ββββββΆβ /api/mcp/token (Token Exchange) β
β β β β
β ββββββΆβ /api/mcp (MCP Endpoint) β
βββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
Step 1: OAuth Discovery Endpoint
Create /.well-known/oauth-authorization-server to expose OAuth metadata.
CRITICAL: This endpoint must NOT be protected by authentication.
// src/app/.well-known/oauth-authorization-server/route.ts
import { getOAuthMetadata } from "@nocoo/base-mcp/auth";
import { NextResponse } from "next/server";
export function GET() {
// Use environment variable or derive from request
const issuer = process.env.AUTH_URL ?? "http://localhost:3000";
const metadata = getOAuthMetadata(issuer);
return NextResponse.json(metadata, {
headers: { "Cache-Control": "public, max-age=3600" },
});
}
The metadata includes:
authorization_endpoint:/api/mcp/authorizetoken_endpoint:/api/mcp/tokenregistration_endpoint:/api/mcp/register
Step 2: Middleware Configuration
Ensure /.well-known/ is publicly accessible without authentication:
// middleware.ts or proxy-logic.ts
const PUBLIC_PATHS = [
"/login",
"/terms",
"/privacy",
"/.well-known/", // OAuth metadata - must be public
];
function isPublicPath(pathname: string): boolean {
return PUBLIC_PATHS.some(p => pathname === p || pathname.startsWith(p));
}
Step 3: OAuth Endpoints
3.1 Dynamic Client Registration (/api/mcp/register)
// src/app/api/mcp/register/route.ts
import { isLoopbackRedirectUri } from "@nocoo/base-mcp/auth";
import { createMcpClient } from "@/data/mcp-clients"; // Your data layer
export async function POST(request: Request) {
const body = await request.json();
// Validate redirect_uris (loopback only for security)
for (const uri of body.redirect_uris) {
if (!isLoopbackRedirectUri(uri)) {
return errorResponse("Only loopback redirect URIs are allowed");
}
}
const client = await createMcpClient(db, {
client_name: body.client_name,
redirect_uris: body.redirect_uris,
grant_types: body.grant_types ?? ["authorization_code"],
});
return jsonResponse({
client_id: client.client_id,
client_name: client.client_name,
redirect_uris: JSON.parse(client.redirect_uris),
grant_types: JSON.parse(client.grant_types),
token_endpoint_auth_method: "none",
}, 201);
}
3.2 Authorization (/api/mcp/authorize)
// src/app/api/mcp/authorize/route.ts
export async function GET(request: Request) {
const url = new URL(request.url);
const params = url.searchParams;
// Required OAuth params
const responseType = params.get("response_type"); // Must be "code"
const clientId = params.get("client_id");
const redirectUri = params.get("redirect_uri");
const codeChallenge = params.get("code_challenge");
const codeChallengeMethod = params.get("code_challenge_method"); // Must be "S256"
const state = params.get("state");
// Store auth session keyed by state
await createAuthSession(db, {
state,
client_id: clientId,
redirect_uri: redirectUri,
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
scope,
expires_at: now + AUTH_CODE_TTL,
});
// Check if user already authenticated
const session = await auth();
if (session?.user?.email) {
return NextResponse.redirect(`/api/mcp/callback?state=${state}`);
}
// Redirect to login
return NextResponse.redirect(`/login?callbackUrl=/api/mcp/callback?state=${state}`);
}
3.3 Callback (/api/mcp/callback)
// src/app/api/mcp/callback/route.ts
export async function GET(request: Request) {
const state = new URL(request.url).searchParams.get("state");
// Verify user is authenticated
const session = await auth();
if (!session?.user?.email) {
return errorResponse("Authentication required", 401);
}
// Look up auth session
const authSession = await getAuthSessionByState(db, state);
if (!authSession) {
return errorResponse("Invalid or expired session");
}
// Generate authorization code
const code = randomBytes(32).toString("hex");
await upgradeAuthSession(db, state, code, session.user.email);
// Redirect to client's redirect_uri
const redirectUrl = new URL(authSession.redirect_uri);
redirectUrl.searchParams.set("code", code);
redirectUrl.searchParams.set("state", state);
return NextResponse.redirect(redirectUrl.toString());
}
3.4 Token Exchange (/api/mcp/token)
// src/app/api/mcp/token/route.ts
import { verifyPkceS256 } from "@nocoo/base-mcp/auth";
export async function POST(request: Request) {
const body = await request.formData();
const grantType = body.get("grant_type");
if (grantType === "authorization_code") {
const code = body.get("code");
const codeVerifier = body.get("code_verifier");
// Look up and validate auth code
const authCode = await getAuthCodeByCode(db, code);
// Verify PKCE
const pkceValid = await verifyPkceS256(codeVerifier, authCode.code_challenge);
if (!pkceValid) {
return oauthError("invalid_grant", "PKCE verification failed");
}
// Issue tokens
return jsonResponse({
access_token: accessToken,
token_type: "Bearer",
expires_in: ACCESS_TOKEN_TTL,
refresh_token: refreshToken,
scope,
});
}
if (grantType === "refresh_token") {
// Handle refresh token exchange
}
}
Step 4: MCP Endpoint
// src/app/api/mcp/route.ts
import { validateMcpToken, validateOrigin } from "@nocoo/base-mcp/auth";
export async function POST(request: Request) {
const siteUrl = process.env.AUTH_URL ?? "http://localhost:3000";
// Step 1: Validate Origin (DNS rebinding protection)
const origin = request.headers.get("origin");
const originResult = validateOrigin(origin, siteUrl);
if (!originResult.valid) {
return errorResponse(originResult.error, originResult.status);
}
// Step 2: Validate Bearer token
const authResult = await validateMcpToken(
db,
request.headers.get("authorization"),
);
if (!authResult.valid) {
return errorResponse(authResult.error, authResult.status);
}
// Step 3: Create MCP server and handle request
const server = createMcpServer(db);
const transport = new WebStandardStreamableHTTPServerTransport({
enableJsonResponse: true, // Stateless mode
});
await server.connect(transport);
return transport.handleRequest(request);
}
Step 5: MCP Configuration Page
Create a user-facing page to help configure MCP clients:
// src/app/mcp-tokens/mcp-tokens-client.tsx
"use client";
function getMcpUrl(): string {
if (typeof window === "undefined") {
return "https://your-app.com/api/mcp";
}
// Dynamic URL derivation - no hardcoding
const { protocol, hostname, port } = window.location;
let baseUrl = `${protocol}//${hostname}`;
if (port && port !== "80" && port !== "443") {
baseUrl += `:${port}`;
}
return `${baseUrl}/api/mcp`;
}
export function McpConfigPage() {
const mcpUrl = useMemo(() => getMcpUrl(), []);
const configs = {
"claude-code": `{
"mcpServers": {
"your-app": {
"type": "http",
"url": "${mcpUrl}"
}
}
}`,
"claude-desktop": `{
"mcpServers": {
"your-app": {
"command": "npx",
"args": ["-y", "mcp-remote", "${mcpUrl}"]
}
}
}`,
};
// Render configuration tabs...
}
Key principles:
- Never hardcode domains - derive from
window.location - Use
type: "http"for Claude Code (supports Streamable HTTP natively) - Use
mcp-remotebridge for Claude Desktop (requires stdio)
Summary Checklist
-
/.well-known/oauth-authorization-serverreturns JSON without auth - Middleware allows
/.well-known/paths through without login -
/api/mcp/registervalidates loopback redirect URIs -
/api/mcp/authorizestores auth session and redirects to login -
/api/mcp/callbackgenerates auth code after login -
/api/mcp/tokenverifies PKCE and issues tokens -
/api/mcpvalidates origin and bearer token - MCP config page derives URL dynamically (no hardcoding)
Quick Start (Entity-Driven CRUD)
import { createMcpServer, registerEntityTools, ok, error } from "@nocoo/base-mcp";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { z } from "zod";
// 1. Create MCP Server
const server = createMcpServer({
name: "my-app",
version: "1.0.0",
});
// 2. Define an Entity
const productEntity = {
name: "product",
display: "Product",
plural: "products",
dataLayer: {
list: async (ctx, opts) => ctx.repos.products.list(opts),
getById: async (ctx, id) => ctx.repos.products.getById(id),
create: async (ctx, input) => ctx.repos.products.create(input),
update: async (ctx, id, input) => ctx.repos.products.update(id, input),
delete: async (ctx, id) => ctx.repos.products.delete(id),
},
schemas: {
list: { category: z.string().optional() },
create: { name: z.string(), price: z.number() },
update: { name: z.string().optional(), price: z.number().optional() },
},
};
// 3. Register Entity Tools
const ctx = { repos: createRepos(db) };
registerEntityTools(server, productEntity, ctx);
// 4. Handle HTTP requests (Hono/Cloudflare Worker example)
app.post("/api/mcp", async (c) => {
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless
enableJsonResponse: true,
});
await server.connect(transport);
return transport.handleRequest(c.req.raw);
});
Server
import { createMcpServer } from "@nocoo/base-mcp";
const server = createMcpServer({
name: "my-server",
version: "1.0.0",
capabilities: { tools: true, resources: false },
});
Framework
import {
// Entity registration
registerEntityTools,
registerCustomTool,
createCrudHandlers,
// Response builders
ok,
error,
// Field projection
projectFields,
// ID/Slug resolution
validateIdOrSlug,
resolveEntity,
isResolveError,
} from "@nocoo/base-mcp";
Auth
import {
// PKCE
verifyPkceS256,
generateCodeVerifier,
generateCodeChallenge,
isLoopbackRedirectUri,
// OAuth Metadata
getOAuthMetadata,
// Origin Validation
validateOrigin,
isLoopbackHost,
// Token Management
generateToken,
hashToken,
tokenPreview,
extractBearerToken,
validateMcpToken,
} from "@nocoo/base-mcp/auth";
Testing
import {
createMockContext,
parseToolResult,
isToolError,
getToolErrorMessage,
createMockTokenStore,
} from "@nocoo/base-mcp/testing";
Entity Configuration
interface EntityConfig<T, TRepos> {
name: string; // Singular name (e.g., "product")
display: string; // Human-readable name
plural: string; // Plural name for list tool
dataLayer: {
list: (ctx, opts) => Promise<T[]>;
getById: (ctx, id) => Promise<T | null>;
getBySlug?: (ctx, slug) => Promise<T | null>;
create?: (ctx, input) => Promise<T>;
update?: (ctx, id, input) => Promise<T | null>;
delete?: (ctx, id) => Promise<boolean>;
};
schemas?: {
list?: Record<string, ZodType>;
create?: Record<string, ZodType>;
update?: Record<string, ZodType>;
};
descriptions?: {
list?: string;
get?: string;
create?: string;
update?: string;
delete?: string;
};
projection?: {
omit: string[];
groups: Record<string, string[]>;
};
hooks?: {
beforeCreate?: (ctx, input) => Promise<Record<string, unknown>>;
afterCreate?: (ctx, entity) => Promise<void>;
beforeUpdate?: (ctx, id, input, existing) => Promise<Record<string, unknown>>;
afterUpdate?: (ctx, id, input, result) => Promise<void>;
beforeDelete?: (ctx, id, existing) => Promise<void>;
afterDelete?: (ctx, id) => Promise<void>;
};
}
License
MIT
