atlas-mcp-server
by cyanheads
Verified
- atlas-mcp-server
- docs
# Creating New MCP Tools in Atlas
This developer guide explains how to create new Model Context Protocol (MCP) tools for the Atlas MCP Server. It covers the entire process from designing your tool's interface to implementing its functionality and registering it with the server.
## Table of Contents
1. [Introduction](#introduction)
2. [Directory Structure](#directory-structure)
3. [Step 1: Define Your Tool's Types](#step-1-define-your-tools-types)
4. [Step 2: Implement the Tool Handler](#step-2-implement-the-tool-handler)
5. [Step 3: Create the Registration File](#step-3-create-the-registration-file)
6. [Step 4: Register with the MCP Server](#step-4-register-with-the-mcp-server)
7. [Error Handling Best Practices](#error-handling-best-practices)
8. [Logging Guidelines](#logging-guidelines)
9. [Testing Your Tool](#testing-your-tool)
10. [Advanced Patterns](#advanced-patterns)
11. [Complete Example](#complete-example)
## Introduction
Atlas MCP Server provides a framework for implementing tools that follow the Model Context Protocol. Tools are executable functions that perform operations with side effects, unlike resources which are read-only. Each tool:
- Has a unique name (e.g., `project_create`)
- Accepts structured input parameters
- Performs operations (database queries, calculations, etc.)
- Returns formatted responses
- Handles errors in a standardized way
This guide will walk you through creating a new tool from scratch.
## Directory Structure
All MCP tools live in the `src/mcp/tools/` directory. Each tool should have its own subdirectory following this structure:
```
src/mcp/tools/
└── yourToolName/
├── types.ts # Type definitions and schemas
├── yourToolName.ts # Implementation of the tool
└── index.ts # Tool registration
```
## Step 1: Define Your Tool's Types
Start by creating the `types.ts` file to define your tool's input and output types using Zod for validation.
```typescript
// src/mcp/tools/yourToolName/types.ts
import { z } from "zod";
// Define validation schema for your tool's inputs
export const YourToolInputSchema = z.object({
// Required fields
requiredParam: z.string().min(1).describe(
"A required parameter with a description"
),
// Optional fields
optionalParam: z.number().optional().describe(
"An optional numeric parameter"
),
// Enum fields
mode: z.enum(["mode1", "mode2"]).describe(
"Operation mode selection"
)
});
// Type definition generated from the schema
export type YourToolInput = z.infer<typeof YourToolInputSchema>;
// For public export (used in registration)
export const YourToolSchema = {
requiredParam: YourToolInputSchema.shape.requiredParam,
optionalParam: YourToolInputSchema.shape.optionalParam,
mode: YourToolInputSchema.shape.mode
};
```
**Key Points:**
- Always use Zod for schema validation
- Provide clear descriptions for each parameter
- Generate TypeScript types from Zod schemas with `z.infer`
- Export a const object with the schema shapes for registration
## Step 2: Implement the Tool Handler
Create the main implementation file (e.g., `yourToolName.ts`) with the tool handler function:
```typescript
// src/mcp/tools/yourToolName/yourToolName.ts
import { logger } from "../../../utils/logger.js";
import { createToolResponse } from "../../../types/mcp.js";
import { McpError, BaseErrorCode } from "../../../types/errors.js";
import { ToolContext } from "../../../utils/security.js";
import { YourToolInput, YourToolInputSchema } from "./types.js";
/**
* MCP tool that performs a specific function
* Provide a clear description of what your tool does
*/
export const yourToolHandler = async (
input: unknown,
context: ToolContext
) => {
try {
// Step 1: Validate input
const validatedInput = YourToolInputSchema.parse(input);
// Step 2: Log the operation
logger.info("Your tool called", {
param: validatedInput.requiredParam,
mode: validatedInput.mode,
requestId: context.requestContext?.requestId
});
// Step 3: Perform the main operation
const result = await processYourToolOperation(validatedInput, context);
// Step 4: Log successful completion
logger.info("Your tool completed successfully", {
resultSummary: "summary of result",
requestId: context.requestContext?.requestId
});
// Step 5: Return formatted response
return createToolResponse(JSON.stringify(result, null, 2));
} catch (error) {
// Handle specific error types
if (error instanceof McpError) {
throw error;
}
// Log errors
logger.error("Error in your tool", {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
requestId: context.requestContext?.requestId
});
// Convert to McpError
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Error in your tool: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
};
/**
* Helper function to process the main operation
* Separating business logic makes the code more testable
*/
const processYourToolOperation = async (
input: YourToolInput,
context: ToolContext
) => {
// Implement your tool's core functionality here
// This might include database operations, calculations, etc.
// Return a result that will be formatted as JSON
return {
success: true,
message: `Operation completed for ${input.requiredParam}`,
data: {
// Operation output data
}
};
};
```
**Key Points:**
- Follow the standard try/catch pattern for error handling
- Use structured logging with the logger utility
- Separate core functionality into helper functions
- Always validate input using the Zod schema
- Return responses using `createToolResponse`
- Use consistent error handling patterns
## Step 3: Create the Registration File
Create an `index.ts` file to handle tool registration:
```typescript
// src/mcp/tools/yourToolName/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { YourToolSchema } from './types.js';
import { yourToolHandler } from './yourToolName.js';
import { registerTool, createToolExample, createToolMetadata } from '../../../types/tool.js';
import { z } from 'zod';
export const registerYourTool = (server: McpServer) => {
registerTool(
server,
"your_tool_name",
"Clear and concise description of what your tool does.",
{
// Input schema
...YourToolSchema
},
yourToolHandler,
createToolMetadata({
examples: [
// Example 1
createToolExample(
{
requiredParam: "example-value",
mode: "mode1"
},
`{
"success": true,
"message": "Operation completed for example-value",
"data": {
"exampleOutput": "value"
}
}`,
"Example description"
),
// Example 2
createToolExample(
{
requiredParam: "another-example",
optionalParam: 42,
mode: "mode2"
},
`{
"success": true,
"message": "Another example output"
}`,
"Another example description"
)
],
requiredPermission: "your:permission",
rateLimit: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 30 // 30 requests per minute
}
})
);
};
```
**Key Points:**
- Use snake_case for tool names (e.g., `your_tool_name`)
- Provide a clear, concise description
- Include practical examples showing input and output
- Set appropriate rate limiting
- Specify required permissions if applicable
## Step 4: Register with the MCP Server
Add your tool registration to the MCP server in `src/mcp/server.ts`:
```typescript
// src/mcp/server.ts
// Add import at the top with other tool imports
import { registerYourTool } from "./tools/yourToolName/index.js";
// ...
// Inside createMcpServer function, add your registration
// Keep the registrations in alphabetical order by tool name
registerYourTool(server); // your_tool_name
```
## Error Handling Best Practices
Atlas uses a standardized error system with error codes:
```typescript
// Error handling patterns
try {
// Operation that might fail
} catch (error) {
// 1. Handle specific application errors
if (error instanceof McpError) {
throw error; // Pass through existing McpErrors
}
// 2. Handle specific domain errors
if (error instanceof Error && error.message.includes('duplicate')) {
throw new McpError(
YourDomainErrorCode.DUPLICATE_ITEM,
"A more helpful error message",
{ details: "Additional context" }
);
}
// 3. Log and convert unknown errors
logger.error("Error during operation", { error });
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Error during operation: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
```
Common error codes:
- `BaseErrorCode.VALIDATION_ERROR`: For invalid input
- `BaseErrorCode.NOT_FOUND`: For resources that don't exist
- `BaseErrorCode.UNAUTHORIZED`: For permission issues
- `BaseErrorCode.INTERNAL_ERROR`: For unexpected errors
Define domain-specific error codes as needed in `src/types/errors.ts`.
## Logging Guidelines
Consistent logging is crucial for monitoring and debugging:
```typescript
// 1. Log operation start
logger.info("Operation starting", {
param: input.param,
requestId: context.requestContext?.requestId
});
// 2. Log important steps
logger.debug("Processing step", {
step: "step name",
details: { /* relevant details */ }
});
// 3. Log successful completion
logger.info("Operation completed", {
result: "summary",
duration: endTime - startTime,
requestId: context.requestContext?.requestId
});
// 4. Log errors
logger.error("Operation failed", {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
context: { /* operation context */ },
requestId: context.requestContext?.requestId
});
```
Key logging guidelines:
- Always include the `requestId` from context for traceability
- Use appropriate log levels (info, debug, warn, error)
- Structure log messages as objects for better searchability
- Don't log sensitive data
- Include enough context to understand what happened
## Testing Your Tool
Test your tool with the MCP client:
```typescript
// Using the MCP tool:
const result = await client.use_mcp_tool({
server_name: "atlas-mcp-server",
tool_name: "your_tool_name",
arguments: {
requiredParam: "test-value",
mode: "mode1"
}
});
```
## Advanced Patterns
### Bulk Operations
For tools that need to handle bulk operations:
```typescript
// Example bulk operation in types.ts
export const YourToolInputSchema = z.object({
mode: z.enum(["single", "bulk"]).describe(
"'single' for one item, 'bulk' for multiple items."
),
// Single mode parameters
name: z.string().min(1).optional().describe(
"Required for single mode: Item name."
),
// Bulk mode parameters
items: z.array(ItemSchema).min(1).max(100).optional().describe(
"Required for bulk mode: Array of 1-100 items."
)
});
// Example implementation
if (validatedInput.mode === 'bulk') {
// Handle bulk operation
const result = await processBulkOperation(validatedInput.items);
return createToolResponse(JSON.stringify(result, null, 2));
} else {
// Handle single operation
const result = await processSingleOperation(validatedInput);
return createToolResponse(JSON.stringify(result, null, 2));
}
```
### Including Related Data
For tools that need to include related data:
```typescript
// Example implementation
const result: Record<string, any> = { ...baseData };
// Add optional related data
if (input.includeRelatedData) {
const relatedData = await fetchRelatedData(input.id);
result.relatedData = relatedData;
}
```
## Complete Example
Below is a complete example demonstrating how to implement a simple `greeting` tool that creates a customized greeting message.
### types.ts
```typescript
import { z } from "zod";
export const GREETING_LANGUAGES = ["english", "spanish", "french"] as const;
export const GreetingInputSchema = z.object({
name: z.string().min(1).describe(
"The name of the person to greet"
),
language: z.enum(GREETING_LANGUAGES).default("english").describe(
"The language to use for the greeting"
),
formal: z.boolean().default(false).describe(
"Whether to use formal language"
)
});
export type GreetingInput = z.infer<typeof GreetingInputSchema>;
export const GreetingSchema = {
name: GreetingInputSchema.shape.name,
language: GreetingInputSchema.shape.language,
formal: GreetingInputSchema.shape.formal
};
```
### greeting.ts
```typescript
import { logger } from "../../../utils/logger.js";
import { createToolResponse } from "../../../types/mcp.js";
import { McpError, BaseErrorCode } from "../../../types/errors.js";
import { ToolContext } from "../../../utils/security.js";
import { GreetingInput, GreetingInputSchema } from "./types.js";
export const greeting = async (
input: unknown,
context: ToolContext
) => {
try {
// Validate input
const validatedInput = GreetingInputSchema.parse(input);
logger.info("Greeting tool called", {
name: validatedInput.name,
language: validatedInput.language,
formal: validatedInput.formal,
requestId: context.requestContext?.requestId
});
// Generate greeting
const result = generateGreeting(validatedInput);
logger.info("Greeting generated", {
result,
requestId: context.requestContext?.requestId
});
return createToolResponse(JSON.stringify({
greeting: result,
timestamp: new Date().toISOString()
}, null, 2));
} catch (error) {
if (error instanceof McpError) {
throw error;
}
logger.error("Error generating greeting", {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
requestId: context.requestContext?.requestId
});
throw new McpError(
BaseErrorCode.INTERNAL_ERROR,
`Error generating greeting: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
};
const generateGreeting = (input: GreetingInput): string => {
const { name, language, formal } = input;
switch (language) {
case "english":
return formal ? `Good day, ${name}.` : `Hello, ${name}!`;
case "spanish":
return formal ? `Buenos días, ${name}.` : `¡Hola, ${name}!`;
case "french":
return formal ? `Bonjour, ${name}.` : `Salut, ${name}!`;
default:
// This shouldn't happen due to enum validation
return `Hello, ${name}!`;
}
};
```
### index.ts
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GreetingSchema } from './types.js';
import { greeting } from './greeting.js';
import { registerTool, createToolExample, createToolMetadata } from '../../../types/tool.js';
export const registerGreetingTool = (server: McpServer) => {
registerTool(
server,
"greeting",
"Generate a customized greeting message in different languages.",
{
...GreetingSchema
},
greeting,
createToolMetadata({
examples: [
createToolExample(
{
name: "Alice"
},
`{
"greeting": "Hello, Alice!",
"timestamp": "2025-03-08T21:34:52.795Z"
}`,
"Simple greeting in English (default)"
),
createToolExample(
{
name: "Carlos",
language: "spanish",
formal: true
},
`{
"greeting": "Buenos días, Carlos.",
"timestamp": "2025-03-08T21:35:12.123Z"
}`,
"Formal greeting in Spanish"
)
],
rateLimit: {
windowMs: 60 * 1000,
maxRequests: 50
}
})
);
};
```
### Update server.ts
```typescript
// Add import
import { registerGreetingTool } from "./tools/greeting/index.js";
// Inside createMcpServer
registerGreetingTool(server); // greeting
```
This guide covers the essential aspects of creating new MCP tools for the Atlas server. By following these patterns and best practices, you'll ensure your tools integrate seamlessly with the existing codebase while maintaining high standards for error handling, logging, and user experience.