MCP Setup Guide
Learn how to integrate the Model Context Protocol (MCP) with OpenSaaS Stack to enable AI assistants like Claude to interact with your application's data.
Overview
The MCP integration in OpenSaaS Stack provides:
- Automatic CRUD Tools - Read, create, update, delete operations for all lists
- Custom Tools - Add specialized operations for your business logic
- Better Auth OAuth - Secure authentication flow with AI assistants
- Access Control - All tools respect your existing access control rules
- Per-List Configuration - Enable/disable tools per list
Prerequisites
Before setting up MCP, you need:
- A working OpenSaaS Stack application
- Better Auth configured (see Authentication Guide)
- Claude Desktop or another MCP client
Installation
MCP functionality is built into the core packages - no additional installation required!
You'll need:
@opensaas/stack-core- MCP handlers and tool generation@opensaas/stack-auth- Better Auth OAuth adapter
Configuration
1. Enable Auth Plugin with MCP
In your opensaas.config.ts, configure the auth plugin with the MCP plugin:
import { config, list } from '@opensaas/stack-core'
import { authPlugin } from '@opensaas/stack-auth'
import { mcp } from '@opensaas/stack-auth/plugins'
export default config({
plugins: [
authPlugin({
emailAndPassword: { enabled: true },
// Add MCP plugin to Better Auth
betterAuthPlugins: [mcp({ loginPage: '/sign-in' })],
}),
],
// ... rest of config
})
2. Enable MCP in Config
Add MCP configuration to your config:
export default config({
// ... plugins
mcp: {
enabled: true,
basePath: '/api/mcp',
auth: {
type: 'better-auth',
loginPage: '/sign-in',
scopes: ['openid', 'profile', 'email'],
},
// Global defaults for all lists
defaultTools: {
read: true,
create: true,
update: true,
delete: true,
},
},
// ... lists
})
3. Configure List-Level Tools
Control which tools are available per list:
lists: {
Post: list({
fields: {
// ... your fields
},
// MCP configuration for this list
mcp: {
tools: {
read: true,
create: true,
update: true,
delete: true,
},
},
}),
}
4. Add Custom Tools (Optional)
Create specialized operations for your lists:
import { z } from 'zod'
lists: {
Post: list({
fields: {
title: text(),
status: select({
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
}),
publishedAt: timestamp(),
},
mcp: {
tools: { read: true, create: true, update: true, delete: true },
// Add custom tools
customTools: [
{
name: 'publishPost',
description: 'Publish a draft post and set publishedAt timestamp',
inputSchema: z.object({
postId: z.string(),
}),
handler: async ({ input, context }) => {
const post = await context.db.post.update({
where: { id: input.postId },
data: {
status: 'published',
publishedAt: new Date(),
},
})
if (!post) {
return {
error: 'Failed to publish post. Access denied or post not found.',
}
}
return {
success: true,
message: `Post "${post.title}" published successfully`,
post,
}
},
},
],
},
}),
}
Setup Route Handlers
1. Create MCP API Route
Create app/api/mcp/[[...transport]]/route.ts:
import { createMcpHandlers } from '@opensaas/stack-core/mcp'
import { createBetterAuthMcpAdapter } from '@opensaas/stack-auth/mcp'
import config from '@/opensaas.config'
import { auth } from '@/lib/auth'
import { getContext } from '@/.opensaas/context'
const { GET, POST, DELETE } = createMcpHandlers({
config: await config,
getSession: createBetterAuthMcpAdapter(auth),
getContext,
})
export { GET, POST, DELETE }
2. Create OAuth Discovery Endpoints
Better Auth provides OAuth endpoints automatically, but some MCP clients need them at specific paths.
Create app/.well-known/oauth-authorization-server/route.ts:
export async function GET(req: Request) {
// Delegate to Better Auth's built-in handler
const authUrl = new URL('/api/auth/.well-known/oauth-authorization-server', req.url)
return fetch(authUrl.toString(), {
headers: req.headers,
})
}
Create app/.well-known/oauth-protected-resource/route.ts:
export async function GET() {
const baseUrl = process.env.BETTER_AUTH_URL || 'http://localhost:3000'
return Response.json({
resource: baseUrl,
authorization_servers: [`${baseUrl}/api/auth`],
})
}
Generate Schema
Run the generator to create your MCP tools:
pnpm generate
This creates:
.opensaas/mcp/tools.json- Tool metadata reference.opensaas/mcp/README.md- Usage instructions
Configure Claude Desktop
Add your MCP server to Claude Desktop's configuration file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-app": {
"url": "http://localhost:3000/api/mcp",
"transport": {
"type": "http"
},
"auth": {
"type": "oauth2",
"authorizationUrl": "http://localhost:3000/.well-known/oauth-authorization-server"
}
}
}
}
For production deployments, replace localhost:3000 with your production URL.
Authentication Flow
When Claude Desktop connects to your MCP server:
- OAuth Discovery - Claude fetches OAuth metadata from
.well-knownendpoints - Authorization - Claude opens your login page in a browser
- User Login - User signs in using your auth UI
- Token Exchange - Better Auth issues access token
- MCP Connection - Claude uses token to authenticate tool calls
- Access Control - All operations respect your access control rules
Generated Tools
The generator creates CRUD tools for each list:
Query Tool
Tool Name: list_{listKey}_query
Description: Query records with filters, sorting, and pagination
Input Schema:
{
where?: Record<string, any>, // Prisma where filters
orderBy?: Record<string, 'asc' | 'desc'>,
take?: number,
skip?: number,
}
Example:
{
"where": { "status": { "equals": "published" } },
"orderBy": { "createdAt": "desc" },
"take": 10
}
Create Tool
Tool Name: list_{listKey}_create
Description: Create a new record
Input Schema:
{
data: Record<string, any> // Fields to set
}
Example:
{
"data": {
"title": "My Post",
"slug": "my-post",
"content": "Post content",
"status": "draft"
}
}
Update Tool
Tool Name: list_{listKey}_update
Description: Update an existing record
Input Schema:
{
where: { id: string },
data: Record<string, any>
}
Example:
{
"where": { "id": "post-123" },
"data": { "status": "published" }
}
Delete Tool
Tool Name: list_{listKey}_delete
Description: Delete a record
Input Schema:
{
where: {
id: string
}
}
Example:
{
"where": { "id": "post-123" }
}
Access Control
All MCP tools respect your existing access control rules defined in opensaas.config.ts.
Operation-Level Access
Controls whether a user can perform an operation at all:
access: {
operation: {
query: ({ session }) => {
// Anonymous users can only see published posts
if (!session) {
return { status: { equals: 'published' } }
}
return true
},
create: isSignedIn,
update: isAuthor,
delete: isAuthor,
},
}
Field-Level Access
Controls which fields can be read or modified:
fields: {
title: text({
access: {
read: () => true,
create: isSignedIn,
update: isAuthor,
},
}),
internalNotes: text({
access: {
read: isAuthor, // Only author can see
create: isAuthor,
update: isAuthor,
},
}),
}
Silent Failures
When access is denied, tools return empty results rather than errors. This prevents information leakage about whether records exist.
Testing MCP Tools
Using Claude Desktop
- Restart Claude Desktop after updating config
- Start a new conversation
- Ask Claude to interact with your data:
Can you show me all published posts?
Can you create a new post titled "Test Post"?
Can you update post-123 to published status?
Using HTTP Requests
Test tools directly via HTTP:
# Get access token (login first to get session)
ACCESS_TOKEN="your-token-here"
# List available tools
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{"method": "tools/list", "params": {}}'
# Call a tool
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"method": "tools/call",
"params": {
"name": "list_post_query",
"arguments": {
"where": { "status": { "equals": "published" } },
"take": 10
}
}
}'
Architecture
┌─────────────────┐
│ Claude Desktop │
└────────┬────────┘
│ OAuth 2.0
↓
┌─────────────────┐
│ Better Auth │ ← MCP Plugin
│ (OAuth Flow) │
└────────┬────────┘
│ Access Token
↓
┌─────────────────┐
│ MCP Server │ ← Generated Tools
│ (.opensaas/mcp) │
└────────┬────────┘
│ context.db
↓
┌─────────────────┐
│ Access Control │ ← Your Rules
│ Engine │
└────────┬────────┘
│
↓
┌─────────────────┐
│ Prisma DB │ ← Your Data
└─────────────────┘
Best Practices
Security
- Always use access control - Never set all operations to
() => truein production - Validate custom tool inputs - Use Zod schemas for type safety
- Use HTTPS in production - OAuth requires secure connections
- Scope tokens appropriately - Only request needed OAuth scopes
Tool Design
- Keep tools focused - Each custom tool should do one thing well
- Provide clear descriptions - Help AI understand when to use each tool
- Return meaningful errors - Help debug issues without leaking sensitive info
- Test with real data - Verify access control works as expected
Performance
- Limit query results - Use
takeparameter to prevent large responses - Index frequently filtered fields - Mark fields with
isIndexed: true - Batch operations carefully - Consider rate limits in custom tools
Troubleshooting
Claude Desktop Can't Find Server
Problem: "Failed to connect to MCP server"
Solutions:
- Verify your app is running:
pnpm dev - Check the MCP URL in
claude_desktop_config.json - Ensure OAuth endpoints are accessible
- Restart Claude Desktop after config changes
Authentication Fails
Problem: OAuth flow redirects but doesn't complete
Solutions:
- Verify
BETTER_AUTH_URLenvironment variable is set - Check that login page exists at the configured path
- Ensure Better Auth is properly configured
- Check browser console for errors during OAuth flow
Tools Return Empty Results
Problem: Tools run but return no data
Possible Causes:
- Access control denying access - Check your access rules
- No matching records - Verify data exists in database
- Session not found - OAuth token may have expired
Debug Steps:
- Check database directly:
pnpm db:studio - Test access control with direct context calls
- Verify session is being passed to context
Custom Tools Not Working
Problem: Custom tool defined but not available
Solutions:
- Run
pnpm generateto regenerate tools - Restart development server
- Verify Zod schema is valid
- Check handler function returns correct format
Example Project
See the complete working example at examples/mcp-demo/ in the repository:
- Full configuration with custom tools
- OAuth setup with Better Auth
- Access control patterns
- Testing scripts
Next Steps
- Authentication Guide - Setup Better Auth
- Access Control - Secure your data
- Hooks System - Add business logic
- Better Auth MCP Plugin - OAuth details
- Model Context Protocol - MCP specification