Registering Custom MCP Tools
Learn how to register custom MCP tools in your Next.js application
Registering Custom MCP Tools
This guide shows you how to add custom MCP tools to your application, allowing AI assistants to interact with your specific business logic.
Creating an MCP Route
First, create an MCP route handler in your Next.js app:
// src/app/api/mcp/route.ts
import { createMcpHandler } from "mcp-handler";
import { auth } from "@repo/auth/server";
import { db } from "@repo/database";
import { headers } from "next/headers";
import { z } from "zod";
async function requireAuth() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user?.id) {
throw new Error("Unauthorized: Authentication required");
}
return session;
}
const handler = createMcpHandler(
(server) => {
// Register your tools here
server.tool(
"get_project_status",
"Get the status of a project by ID",
{
projectId: z.string(),
},
async ({ projectId }) => {
try {
await requireAuth();
const project = await db()
.select()
.from(projects)
.where(eq(projects.id, projectId))
.limit(1);
if (!project[0]) {
return {
content: [
{
type: "text",
text: `Project ${projectId} not found`,
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(project[0], null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
);
},
{},
{ basePath: "/api" },
);
export { handler as GET, handler as POST };Tool Registration Pattern
Each tool follows the server.tool() method with four parameters:
server.tool(
name: string, // Unique tool identifier
description: string, // Description for the AI model
parameters: ZodSchema, // Zod schema for validation
handler: Function // Async function that executes the tool
);Parameter Validation with Zod
Use Zod schemas to define and validate tool parameters:
import { z } from "zod";
// Simple parameters
server.tool(
"get_user",
"Get user by ID",
{
userId: z.string(),
},
async ({ userId }) => {
// userId is guaranteed to be a string
},
);
// Optional parameters with defaults
server.tool(
"list_items",
"List items with pagination",
{
limit: z.number().int().min(1).max(100).optional(),
offset: z.number().int().min(0).optional(),
},
async ({ limit = 50, offset = 0 }) => {
// limit defaults to 50, offset to 0
},
);
// Complex objects
server.tool(
"create_task",
"Create a new task",
{
title: z.string().min(1).max(200),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high"]),
dueDate: z.string().datetime().optional(),
assigneeIds: z.array(z.string()).optional(),
},
async ({ title, description, priority, dueDate, assigneeIds }) => {
// All parameters are validated
},
);Return Format
Tools must return content in a specific format:
return {
content: [
{
type: "text",
text: "Your response text here",
},
],
};For structured data, use JSON.stringify():
return {
content: [
{
type: "text",
text: JSON.stringify(
{
id: user.id,
name: user.name,
email: user.email,
},
null,
2,
),
},
],
};Error Handling
Always wrap tool logic in try-catch blocks:
server.tool(
"risky_operation",
"Perform a database operation",
{ id: z.string() },
async ({ id }) => {
try {
await requireAuth();
const result = await performOperation(id);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
);Complete Example: Email Tools
Here's a complete example for email-related MCP tools:
// src/app/api/mcp/email/route.ts
import { createMcpHandler } from "mcp-handler";
import { db } from "@repo/database";
import { email_message, email_thread } from "@repo/database/schema";
import { eq, desc } from "@repo/database";
import { z } from "zod";
const handler = createMcpHandler(
(server) => {
// Get recent emails for an inbox
server.tool(
"get_recent_emails",
"Get recent emails from an inbox",
{
inboxId: z.string(),
limit: z.number().int().min(1).max(50).optional(),
},
async ({ inboxId, limit = 10 }) => {
try {
const emails = await db()
.select()
.from(email_message)
.where(eq(email_message.inboxId, inboxId))
.orderBy(desc(email_message.receivedAt))
.limit(limit);
return {
content: [
{
type: "text",
text: JSON.stringify(emails, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
);
// Search emails by subject
server.tool(
"search_emails_by_subject",
"Search emails by subject line",
{
query: z.string().min(1),
inboxId: z.string().optional(),
limit: z.number().int().min(1).max(50).optional(),
},
async ({ query, inboxId, limit = 20 }) => {
try {
let query_builder = db()
.select()
.from(email_message)
.where(sql`${email_message.subject} ILIKE ${`%${query}%`}`)
.orderBy(desc(email_message.receivedAt))
.limit(limit);
if (inboxId) {
query_builder = query_builder.where(
eq(email_message.inboxId, inboxId),
);
}
const emails = await query_builder;
return {
content: [
{
type: "text",
text: JSON.stringify(emails, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
},
);
},
{},
{ basePath: "/api" },
);
export { handler as GET, handler as POST };Testing Your Tools
You can test MCP tools using curl:
# Test the MCP endpoint
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Cookie: your-session-cookie" \
-d '{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get_project_status",
"arguments": {
"projectId": "proj_123"
}
},
"id": 1
}'Best Practices
- Use descriptive names - Tool names should clearly indicate what they do
- Write clear descriptions - The AI uses descriptions to decide which tool to call
- Validate inputs - Always use Zod schemas to validate parameters
- Handle errors gracefully - Return meaningful error messages
- Protect sensitive data - Always authenticate and authorize tool access
- Keep tools focused - Each tool should do one thing well
- Return structured data - Use JSON for complex responses
Next Steps
- MCP Tools Overview - Learn more about MCP tools
- ChatGPT SDK Overview - Overview of the ChatGPT SDK integration