The MCP DevTools server is designed to be easily extensible with new tools. This section provides detailed guidance on how to create and integrate new tools into the server.
MCP DevTools uses the mcp-go library.
You shouldn't need to read the mcp-go documentation to add a simple tool, but it may be useful if you want to understand specific features or functionality in more detail.
- https://mcp-go.dev/servers/tools (MCP tools)
- https://mcp-go.dev/servers/resources (MCP resources)
- https://mcp-go.dev/servers/advanced (typed tools, middleware, hooks, filtering, notifications, client capability filtering)
All tools must implement the tools.Tool interface defined in internal/tools/tools.go:
type Tool interface {
// Definition returns the tool's definition for MCP registration
Definition() mcp.Tool
// Execute executes the tool's logic
Execute(ctx context.Context, logger *logrus.Logger, cache *sync.Map, args map[string]any) (*mcp.CallToolResult, error)
}A typical tool implementation follows this structure:
- Tool Type: Define a struct that will implement the Tool interface
- Registration: Register the tool with the registry in an
init()function - Definition: Implement the
Definition()method to define the tool's name, description, and parameters - Execution: Implement the
Execute()method to perform the tool's logic
Create a new package in the appropriate category under internal/tools/ or create a new category if needed:
mkdir -p internal/tools/your-category/your-tool
touch internal/tools/your-category/your-tool/your-tool.goHere's a template for implementing a new tool:
package yourtool
import (
"context"
"fmt"
"sync"
"github.com/mark3labs/mcp-go/mcp"
"github.com/sammcj/mcp-devtools/internal/registry"
"github.com/sammcj/mcp-devtools/internal/security"
"github.com/sirupsen/logrus"
)
// YourTool implements the tools.Tool interface
type YourTool struct {
// Add any fields your tool needs here
}
// init registers the tool with the registry
func init() {
registry.Register(&YourTool{})
}
// Definition returns the tool's definition for MCP registration
func (t *YourTool) Definition() mcp.Tool {
return mcp.NewTool(
"your_tool_name",
mcp.WithDescription("Description of your tool"),
// Define required parameters
mcp.WithString("param1",
mcp.Required(),
mcp.Description("Description of param1"),
),
// Define optional parameters
mcp.WithNumber("param2",
mcp.Description("Description of param2"),
mcp.DefaultNumber(10),
),
// Add more parameters as needed
)
}
// Execute executes the tool's logic
func (t *YourTool) Execute(ctx context.Context, logger *logrus.Logger, cache *sync.Map, args map[string]any) (*mcp.CallToolResult, error) {
// Log the start of execution
logger.Info("Executing your tool")
// Parse parameters
param1, ok := args["param1"].(string)
if !ok {
return nil, fmt.Errorf("missing required parameter: param1")
}
// Parse optional parameters with defaults
param2 := float64(10)
if param2Raw, ok := args["param2"].(float64); ok {
param2 = param2Raw
}
// SECURITY INTEGRATION: Check file access if your tool reads files
if needsFileAccess {
if err := security.CheckFileAccess(filePath); err != nil {
return nil, err
}
}
// SECURITY INTEGRATION: Check domain access if your tool makes HTTP requests
if needsDomainAccess {
if err := security.CheckDomainAccess(domain); err != nil {
return nil, err
}
}
// Implement your tool's logic here
content := "fetched or processed content"
// SECURITY INTEGRATION: Analyse content for security risks
source := security.SourceContext{
Tool: "your_tool_name",
Domain: domain, // for HTTP content
Source: filePath, // for file content
Type: "content_type",
}
if result, err := security.AnalyseContent(content, source); err == nil {
switch result.Action {
case security.ActionBlock:
return nil, fmt.Errorf("content blocked by security policy: %s", result.Message)
case security.ActionWarn:
logger.WithField("security_id", result.ID).Warn(result.Message)
}
}
result := map[string]any{
"message": fmt.Sprintf("Tool executed with param1=%s, param2=%f", param1, param2),
"content": content,
// Add more result fields as needed
}
// Return the result
return mcp.NewCallToolResult(result), nil
}The MCP framework supports various parameter types:
- String:
mcp.WithString("name", ...) - Number:
mcp.WithNumber("name", ...) - Boolean:
mcp.WithBoolean("name", ...) - Array:
mcp.WithArray("name", ...) - Object:
mcp.WithObject("name", ...)
For each parameter, you can specify:
- Required:
mcp.Required()- Mark the parameter as required - Description:
mcp.Description("...")- Provide a description - Default Value:
mcp.DefaultString("..."),mcp.DefaultNumber(10),mcp.DefaultBool(false)- Set a default value - Enum:
mcp.Enum("value1", "value2", ...)- Restrict to a set of values - Properties:
mcp.Properties(map[string]any{...})- Define properties for object parameters
The result of a tool execution should be a *mcp.CallToolResult object, which can be created with:
mcp.NewCallToolResult(result)Where result is a map[string]any containing the tool's output data.
For structured results, you can use:
// Define a result struct
type Result struct {
Message string `json:"message"`
Count int `json:"count"`
}
// Create a result
result := Result{
Message: "Tool executed successfully",
Count: 42,
}
// Convert to JSON
resultJSON, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
// Create a CallToolResult
return mcp.NewCallToolResultJSON(resultJSON)The cache parameter in the Execute method is a shared cache that can be used to store and retrieve data across tool executions:
// Store a value in the cache
cache.Store("key", value)
// Retrieve a value from the cache
if cachedValue, ok := cache.Load("key"); ok {
// Use cachedValue
}IMPORTANT: All tools that access files or make HTTP requests MUST integrate with the security system. This provides protection against malicious content and unauthorized access.
The preferred approach is to use security helper functions that provide simplified APIs with automatic security integration and content integrity preservation.
For HTTP operations:
// Create operations instance for your tool
ops := security.NewOperations("your_tool_name")
// Secure HTTP GET with content integrity preservation
safeResp, err := ops.SafeHTTPGet(urlStr)
if err != nil {
// Handle security blocks or network errors
if secErr, ok := err.(*security.SecurityError); ok {
return nil, security.FormatSecurityBlockError(secErr)
}
return nil, err
}
// Content is EXACT bytes from server
content := safeResp.Content
// Check for security warnings (non-blocking)
if safeResp.SecurityResult != nil && safeResp.SecurityResult.Action == security.ActionWarn {
logger.Warnf("Security warning [ID: %s]: %s", safeResp.SecurityResult.ID, safeResp.SecurityResult.Message)
// Content is still available despite warning
}
// Process exact content
return processContent(content)For file operations:
ops := security.NewOperations("your_tool_name")
// Secure file read with content integrity preservation
safeFile, err := ops.SafeFileRead(filePath)
if err != nil {
// Handle security blocks or file errors
if secErr, ok := err.(*security.SecurityError); ok {
return nil, security.FormatSecurityBlockError(secErr)
}
return nil, err
}
// Content is EXACT file bytes
content := safeFile.Content
// Handle security warnings if present
if safeFile.SecurityResult != nil && safeFile.SecurityResult.Action == security.ActionWarn {
logger.Warnf("Security warning [ID: %s]: %s", safeFile.SecurityResult.ID, safeFile.SecurityResult.Message)
}
return processContent(content)- 80-90% Boilerplate Reduction: From 30+ lines to 5-10 lines
- Content Integrity: Guaranteed exact byte preservation
- Security Compliance: Automatic integration with security framework
- Error Handling: Consistent security error patterns
- Performance: Same security guarantees with simpler code
For tools requiring fine-grained control, you can manually integrate with the security system:
File Access Security:
// Before any file operation
if err := security.CheckFileAccess(filePath); err != nil {
return nil, err // Access denied by security policy
}Domain Access Security:
// Before making HTTP requests
if err := security.CheckDomainAccess(domain); err != nil {
return nil, err // Domain blocked by security policy
}Content Analysis Security:
// After fetching/processing content
source := security.SourceContext{
Tool: "your_tool_name",
Domain: domain, // for HTTP content
Source: filePath, // for file content
Type: "content_type", // e.g., "web_content", "file_content", "api_response"
}
if result, err := security.AnalyseContent(content, source); err == nil {
switch result.Action {
case security.ActionBlock:
return nil, fmt.Errorf("content blocked by security policy: %s", result.Message)
case security.ActionWarn:
logger.WithField("security_id", result.ID).Warn(result.Message)
// Continue processing but log the warning
case security.ActionAllow:
// Content is safe, continue normally
}
}For Helper Functions (Recommended):
- Import
"github.com/sammcj/mcp-devtools/internal/security" - Create Operations instance:
ops := security.NewOperations("tool_name") - Use
ops.SafeHTTPGet/Post()for HTTP operations - Use
ops.SafeFileRead/Write()for file operations - Handle
SecurityErrorin error responses - Log security warnings when present
- Process exact content from response types
For Manual Integration:
- Import
"github.com/sammcj/mcp-devtools/internal/security" - Call
security.CheckFileAccess()before file operations - Call
security.CheckDomainAccess()before HTTP requests - Call
security.AnalyseContent()for returned content - Handle
ActionBlockby returning an error - Handle
ActionWarnby logging with security ID - Provide appropriate
SourceContextfor content analysis
- Disabled by default: Security checks are no-ops when security is not enabled
- Graceful degradation: Tools work normally when security is disabled
- Override capability: Blocked content includes security IDs for potential overrides
- Audit logging: All security events are logged for review
Add your tool package to the imports registry so it gets automatically loaded. Add the import to internal/imports/tools.go:
import (
// ... existing imports ...
_ "github.com/sammcj/mcp-devtools/internal/tools/your-category/your-tool"
)Important: Do NOT add your tool import directly to main.go. Use the imports registry system instead to ensure proper build tag handling and maintainability.
Here's a simple "Hello World" tool example:
package hello
import (
"context"
"fmt"
"sync"
"github.com/mark3labs/mcp-go/mcp"
"github.com/sammcj/mcp-devtools/internal/registry"
"github.com/sirupsen/logrus"
)
// HelloTool implements a simple hello world tool
type HelloTool struct{}
// init registers the tool with the registry
func init() {
registry.Register(&HelloTool{})
}
// Definition returns the tool's definition for MCP registration
func (t *HelloTool) Definition() mcp.Tool {
return mcp.NewTool(
"hello_world",
mcp.WithDescription("A simple hello world tool"),
mcp.WithString("name",
mcp.Description("Name to greet"),
mcp.DefaultString("World"),
),
)
}
// Execute executes the tool's logic
func (t *HelloTool) Execute(ctx context.Context, logger *logrus.Logger, cache *sync.Map, args map[string]any) (*mcp.CallToolResult, error) {
// Parse parameters
name := "World"
if nameRaw, ok := args["name"].(string); ok && nameRaw != "" {
name = nameRaw
}
// Create result
result := map[string]any{
"message": fmt.Sprintf("Hello, %s!", name),
}
// Return the result
return mcp.NewCallToolResult(result), nil
}To test your tool:
- Build the server:
make build - Run the server:
make run - Send a request to the server:
{
"name": "your_tool_name",
"arguments": {
"param1": "value1",
"param2": 42
}
}After implementing your tool, verify its context overhead doesn't exceed the configured threshold:
# Test your tool's token cost (enable it first if disabled by default)
ENABLE_ADDITIONAL_TOOLS=your_tool_name make benchmark-tokensIf your tool exceeds the default 800 token threshold, consider:
- Simplifying parameter descriptions (keep under 200 characters)
- Reducing enum values or moving detailed information to Extended Help
- Using more concise parameter names
The benchmark shows description vs parameter token breakdown to help identify optimisation opportunities.
The project includes unit tests for core functionality. Tests are designed to be lightweight and fast, avoiding external dependencies.
# Run all tests
make test
# Run only fast tests (no external dependencies)
make test-fastFor tools with complex parameter structures or usage patterns, you can implement the optional ExtendedHelpProvider interface to provide detailed usage information accessible through the get_tool_help tool.
Note: The extended help is not automatically visible to agents, they have to explicitly call the get_tool_help tool to retrieve it.
To add extended help to your tool, implement the ExtendedHelpProvider interface:
import "github.com/sammcj/mcp-devtools/internal/tools"
// Add the ProvideExtendedInfo method to your tool
func (t *YourTool) ProvideExtendedInfo() *tools.ExtendedHelp {
return &tools.ExtendedHelp{
Examples: []tools.ToolExample{
{
Description: "Basic usage example",
Arguments: map[string]any{
"param1": "example_value",
"param2": 42,
},
ExpectedResult: "Description of what this example returns",
},
// Add more examples for different use cases
},
CommonPatterns: []string{
"Start with basic parameters before using advanced options",
"Use parameter X for Y scenario",
"Combine with other tools for complete workflows",
},
Troubleshooting: []tools.TroubleshootingTip{
{
Problem: "Common error or issue users might encounter",
Solution: "How to resolve this issue step by step",
},
},
ParameterDetails: map[string]string{
"param1": "Detailed explanation of param1 with examples and constraints",
"param2": "Advanced usage information for param2 including edge cases",
},
WhenToUse: "Describe when this tool is the right choice",
WhenNotToUse: "Describe when other tools would be better alternatives",
}
}- Examples: Provide 3-5 real-world examples showing different usage patterns with expected results
- CommonPatterns: List workflow patterns and best practices for using the tool effectively
- Troubleshooting: Address common errors and their solutions
- ParameterDetails: Explain complex parameters that need more context than the basic description
- WhenToUse/WhenNotToUse: Help AI agents and less capable AI models understand appropriate tool selection
Consider adding extended help for tools that have:
- Multiple parameter combinations with different behaviours
- Complex parameter structures (nested objects, arrays with specific formats)
- Integration patterns with other tools
- Common error conditions or edge cases
- Context-sensitive behaviour based on available resources/configurations
Tools with extended help:
- Appear in the
get_tool_helptool for discoverability - Provide rich context for AI agents to use tools more effectively
- Reduce trial-and-error by providing clear examples and patterns
- Prevent common mistakes through proactive troubleshooting guidance
Annotations help MCP clients understand tool behaviour and make informed decisions about tool usage.
- ReadOnlyHint: Indicates whether a tool modifies its environment
- DestructiveHint: Marks tools that may perform destructive operations
- IdempotentHint: Shows if repeated calls with same arguments have additional effects
- OpenWorldHint: Indicates whether tools interact with external systems
Read-Only Tools (safe, no side effects):
- Calculator, Internet Search, Web Fetch, Package Documentation, Think Tool
- Annotations:
readOnly: true, destructive: false, openWorld: varies
Non-Destructive Writing Tools (create content, don't destroy):
- Generate Changelog, Document Processing, PDF Processing, Memory Storage
- Annotations:
readOnly: false, destructive: false/true, openWorld: varies
Potentially Destructive Tools (can modify/delete files or execute commands):
- Filesystem Operations, Security Override, Agent Tools (Claude, Codex, Gemini, Q Developer)
- Annotations:
readOnly: false, destructive: true, openWorld: true - Note: These tools require
ENABLE_ADDITIONAL_TOOLSenvironment variable
MCP DevTools includes an optional tool error logging feature that captures detailed information about failed tool calls. This helps identify patterns in tool failures and improve tool reliability over time.
When enabled, any tool execution that returns an error will be logged to a dedicated log file at ~/.mcp-devtools/logs/tool-errors.log.
To enable tool error logging, set the LOG_TOOL_ERRORS environment variable to true
If you want to view the tool descriptions, parameters and annotations as a MCP client would see it, you can optionally run make list-tools.
- You must remember to register tools so that MCP clients can discover them.
- Tool descriptions should focus on WHAT the tool does. Tool descriptions should be action-oriented and concise. For example:
- ✅ Good: "Returns source code structure by stripping function/method bodies whilst preserving signatures, types, and declarations."
- ❌ Poor: "Transform source code by removing implementation details while preserving structure. Achieves 60-80% token reduction for optimising AI context windows"
- The first describes what the tool does; the second explains why it's useful (which bloats the context unnecessarily)
- Parameter descriptions should be clear and specific about the expected input format and constraints
- Tool descriptions should aim to be under 200 characters where possible; save detailed usage information for Extended Help
- If you want to create a function to help with debugging to testing a tool but don't want to expose it to MCP clients using the server, you can do so, just make sure you add a comment that it is a function not intended to be exposed to MCP clients.
- Tool responses should be limited to only include information that is actually useful, there's no point in returning the information an agent provides to call the tool back to them, or any generic information or null / empty fields - these just waste tokens.
- All tools should work on both macOS and Linux unless otherwise specified (we do not need to support Windows).
- Rather than creating lots of tools for one purpose / provider, instead favour creating a single tool with multiple functions and parameters.
- Tools should have fast, concise unit tests that do not rely on external dependencies or services.
- No tool should ever log to stdout or stderr when the MCP server is running in stdio mode as this breaks the MCP protocol.
- Tool enablement decision: By default, ALL new tools should be DISABLED by default unless explicitly approved by Sam, if approved tool gets enabled by being added to the
defaultToolslist in theenabledByDefault()function inregistry.go. Having tools disabled by default follows the secure-by-default principle and helps to prevent context bloat. - You should update docs/tools/overview.md with adding or changing a tool.
- SECURITY: All tools that access files or make HTTP requests MUST integrate with the security system. See Security Integration above and Security System Documentation for details.
- Follow least privilege security principles.