| name | add-mcp-tools |
| description | Guide for adding new MCP tools with consistent patterns for schemas, tool definitions, registry updates, and Better Auth integration |
Adding New MCP Tools to Studio
This guide documents the pattern for adding new MCP tools to Studio codebase. Follow this checklist to ensure consistency with existing tools.
Overview
MCP tools are exposed via the Model Context Protocol and allow programmatic management of resources. Each tool follows a consistent pattern with:
- Zod schemas for input/output validation
defineTool for automatic tracing, metrics, and audit logging
- Access control via
ctx.access.check()
- Better Auth integration via
ctx.boundAuth
File Structure
When adding a new domain of tools (e.g., apiKeys, webhooks, secrets), create the following structure:
apps/mesh/src/tools/<domain>/
├── schema.ts # Zod schemas for entities and operations
├── create.ts # <DOMAIN>_CREATE tool
├── list.ts # <DOMAIN>_LIST tool
├── update.ts # <DOMAIN>_UPDATE tool
├── delete.ts # <DOMAIN>_DELETE tool
└── index.ts # Barrel export
Step-by-Step Checklist
1. Create Schema File (schema.ts)
Define Zod schemas for:
- Entity schema: The resource returned in list/get operations
- Create input/output schemas: Input for creation, output includes created entity
- Update input/output schemas: Partial updates
- Delete input/output schemas: ID input, success confirmation output
import { z } from "zod";
export const MyEntitySchema = z.object({
id: z.string(),
name: z.string(),
createdAt: z.union([z.string(), z.date()]),
});
export type MyEntity = z.infer<typeof MyEntitySchema>;
export const MyCreateInputSchema = z.object({
name: z.string().min(1).max(255),
});
export const MyCreateOutputSchema = z.object({
item: MyEntitySchema,
});
export const MyListInputSchema = z.object({
});
export const MyListOutputSchema = z.object({
items: z.array(MyEntitySchema),
});
export const MyUpdateInputSchema = z.object({
id: z.string(),
});
export const MyUpdateOutputSchema = z.object({
item: MyEntitySchema,
});
export const MyDeleteInputSchema = z.object({
id: z.string(),
});
export const MyDeleteOutputSchema = z.object({
success: z.boolean(),
id: z.string(),
});
2. Create Tool Files
Each tool file follows this pattern:
import { defineTool } from "../../core/define-tool";
import { getUserId, requireAuth, requireOrganization } from "../../core/studio-context";
import { MyCreateInputSchema, MyCreateOutputSchema } from "./schema";
export const MY_DOMAIN_CREATE = defineTool({
name: "MY_DOMAIN_CREATE",
description: "Create a new resource",
inputSchema: MyCreateInputSchema,
outputSchema: MyCreateOutputSchema,
handler: async (input, ctx) => {
requireAuth(ctx);
const organization = requireOrganization(ctx);
await ctx.access.check();
const userId = getUserId(ctx);
if (!userId) {
throw new Error("User ID required");
}
const result = await ctx.boundAuth.myDomain.create({ ... });
const result = await ctx.storage.myDomain.create({ ... });
return { item: result };
},
});
3. Create Barrel Export (index.ts)
export { MY_DOMAIN_CREATE } from "./create";
export { MY_DOMAIN_LIST } from "./list";
export { MY_DOMAIN_UPDATE } from "./update";
export { MY_DOMAIN_DELETE } from "./delete";
export * from "./schema";
4. Update Tool Registry (registry.ts)
Add tool names and metadata:
export type ToolCategory = "Organizations" | "Connections" | "My Domain";
const ALL_TOOL_NAMES = [
"MY_DOMAIN_CREATE",
"MY_DOMAIN_LIST",
"MY_DOMAIN_UPDATE",
"MY_DOMAIN_DELETE",
] as const;
export const MANAGEMENT_TOOLS: ToolMetadata[] = [
{
name: "MY_DOMAIN_CREATE",
description: "Create resource",
category: "My Domain",
},
{
name: "MY_DOMAIN_LIST",
description: "List resources",
category: "My Domain",
},
{
name: "MY_DOMAIN_UPDATE",
description: "Update resource",
category: "My Domain",
},
{
name: "MY_DOMAIN_DELETE",
description: "Delete resource",
category: "My Domain",
dangerous: true,
},
];
const TOOL_LABELS: Record<ToolName, string> = {
MY_DOMAIN_CREATE: "Create resource",
MY_DOMAIN_LIST: "List resources",
MY_DOMAIN_UPDATE: "Update resource",
MY_DOMAIN_DELETE: "Delete resource",
};
export function getToolsByCategory() {
const grouped: Record<string, ToolMetadata[]> = {
Organizations: [],
Connections: [],
"My Domain": [],
};
}
5. Register Tools (tools/index.ts)
import * as MyDomainTools from "./myDomain";
export { MyDomainTools };
export const ALL_TOOLS = [
MyDomainTools.MY_DOMAIN_CREATE,
MyDomainTools.MY_DOMAIN_LIST,
MyDomainTools.MY_DOMAIN_UPDATE,
MyDomainTools.MY_DOMAIN_DELETE,
] as const satisfies { name: ToolName }[];
6. Add to Default Permissions (if needed)
apiKey({
permissions: {
defaultPermissions: {
self: [
"MY_DOMAIN_LIST",
],
},
},
}),
7. Add Better Auth Integration (if wrapping Better Auth API)
If your tools wrap Better Auth APIs, you need to:
a. Add types to studio-context.ts:
export type MyDomainCreateResult = Awaited<
ReturnType<BetterAuthApi["createMyDomain"]>
>;
export interface BoundAuthClient {
myDomain: {
create(data: { ... }): Promise<MyDomainCreateResult>;
list(): Promise<MyDomainListResult>;
update(data: { ... }): Promise<MyDomainUpdateResult>;
delete(id: string): Promise<void>;
};
}
b. Implement in context-factory.ts:
function createBoundAuthClient(ctx: AuthContext): BoundAuthClient {
return {
myDomain: {
create: async (data) => {
return auth.api.createMyDomain({ headers, body: data });
},
list: async () => {
return auth.api.listMyDomain({ headers });
},
update: async (data) => {
return auth.api.updateMyDomain({ headers, body: data });
},
delete: async (id) => {
await auth.api.deleteMyDomain({ headers, body: { id } });
},
},
};
}
8. Document in Spec
Add documentation to apps/mesh/spec/001.md:
#### My Domain Management
Description of what this domain manages.
**MY_DOMAIN_CREATE**
\`\`\`typescript
// Create example
POST /mcp/tools/MY_DOMAIN_CREATE
{ "name": "Example" }
// Response
{ "item": { "id": "...", "name": "Example", ... } }
\`\`\`
// ... other tools
Key Patterns to Follow
Authentication & Authorization
requireAuth(ctx);
const org = requireOrganization(ctx);
await ctx.access.check();
Error Handling
if (!result) {
throw new Error(`Resource not found: ${id}`);
}
if (result.organizationId !== organization.id) {
throw new Error("Resource not found in organization");
}
Sensitive Data
For sensitive data (like API key values):
export const CreateOutputSchema = z.object({
id: z.string(),
secretValue: z.string(),
});
export const EntitySchema = z.object({
id: z.string(),
});
Testing
Create test files alongside tool files:
apps/mesh/src/tools/<domain>/
├── create.test.ts
├── list.test.ts
├── update.test.ts
└── delete.test.ts
Use the existing test patterns from apps/mesh/src/tools/connection/ as reference.
Common Mistakes to Avoid
- Forgetting
await ctx.access.check() - Always check authorization
- Missing tool registration - Update both
registry.ts AND tools/index.ts
- Inconsistent naming - Use
DOMAIN_ACTION pattern (e.g., API_KEY_CREATE)
- Missing default permissions - Add to
auth/index.ts if users should have access by default
- Exposing sensitive data - Only return secrets at creation time
- Missing organization check - Use
requireOrganization(ctx) for org-scoped resources