Building MCP Tools
Tools are the heart of your MCP server. Well-designed tools help the AI understand your APIs and use them effectively. This guide covers best practices for creating tools that work well with Diosc.
Tool Anatomy
Every tool has three parts:
{
// 1. Identity
name: "search_orders",
// 2. Description (for the AI)
description: "Search for orders by customer name, status, or date range. Returns a list of matching orders with their details.",
// 3. Input Schema (JSON Schema)
inputSchema: {
type: "object",
properties: {
customer: {
type: "string",
description: "Customer name to search for (partial match supported)"
},
status: {
type: "string",
enum: ["pending", "processing", "shipped", "delivered", "cancelled"],
description: "Filter by order status"
},
fromDate: {
type: "string",
format: "date",
description: "Start of date range (YYYY-MM-DD)"
},
toDate: {
type: "string",
format: "date",
description: "End of date range (YYYY-MM-DD)"
}
}
}
}
Writing Effective Descriptions
The description is how the AI decides when to use your tool. Good descriptions:
Be Specific About Purpose
// ❌ Vague
description: "Gets order data"
// ✅ Specific
description: "Search for orders by customer name, status, or date range. Use this when the user wants to find orders or check order history."
Include Usage Hints
// ❌ Just the action
description: "Updates order status"
// ✅ With context
description: "Update the status of an order (e.g., from 'pending' to 'shipped'). Use this when the user wants to change an order's state. Requires the order ID and new status."
Mention Limitations
description: "Search for orders from the last 90 days. For older orders, use search_archived_orders instead. Returns maximum 100 results."
Explain Side Effects
description: "Cancel an order and issue a refund. This action cannot be undone. The customer will receive an email notification."
Designing Input Schemas
Use Descriptive Parameter Names
// ❌ Cryptic
{ "cid": "string", "st": "string" }
// ✅ Clear
{ "customerId": "string", "status": "string" }
Add Parameter Descriptions
properties: {
customerId: {
type: "string",
description: "The unique customer identifier (e.g., 'cust_abc123')"
},
includeDetails: {
type: "boolean",
description: "If true, include full order details. If false, return summary only.",
default: false
}
}
Use Enums for Known Values
properties: {
status: {
type: "string",
enum: ["pending", "processing", "shipped", "delivered", "cancelled"],
description: "Order status to filter by"
},
priority: {
type: "string",
enum: ["low", "normal", "high", "urgent"],
default: "normal"
}
}
Specify Required Fields
inputSchema: {
type: "object",
properties: {
orderId: { type: "string" },
status: { type: "string" }
},
required: ["orderId", "status"]
}
Use Appropriate Types
properties: {
// Numbers
quantity: { type: "integer", minimum: 1, maximum: 1000 },
price: { type: "number", minimum: 0 },
// Dates
orderDate: { type: "string", format: "date" },
createdAt: { type: "string", format: "date-time" },
// Arrays
tags: {
type: "array",
items: { type: "string" },
description: "List of tags to filter by"
},
// Objects
address: {
type: "object",
properties: {
street: { type: "string" },
city: { type: "string" },
country: { type: "string" }
}
}
}
Tool Response Format
MCP tools return content blocks:
// Text response (most common)
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
// Error response
return {
isError: true,
content: [{
type: "text",
text: "Order not found. Please check the order ID and try again."
}]
};
// Multiple content blocks
return {
content: [
{ type: "text", text: "Found 3 orders:" },
{ type: "text", text: JSON.stringify(orders) }
]
};
Formatting Responses
Help the AI present data clearly:
// ❌ Raw JSON dump
return { content: [{ type: "text", text: JSON.stringify(orders) }] };
// ✅ Structured for AI consumption
const formatted = orders.map(o =>
`Order #${o.id}: ${o.status} - ${o.total} (${o.items.length} items)`
).join('\n');
return {
content: [{
type: "text",
text: `Found ${orders.length} orders:\n\n${formatted}`
}]
};
Common Tool Patterns
Search/List Tool
{
name: "search_products",
description: "Search for products by name, category, or attributes. Returns matching products with prices and availability.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (searches name and description)"
},
category: {
type: "string",
description: "Filter by category"
},
inStock: {
type: "boolean",
description: "If true, only show in-stock items"
},
limit: {
type: "integer",
default: 10,
maximum: 50,
description: "Maximum results to return"
}
}
}
}
Get Single Item Tool
{
name: "get_order",
description: "Get full details for a specific order by ID. Use search_orders first if you don't know the order ID.",
inputSchema: {
type: "object",
properties: {
orderId: {
type: "string",
description: "The order ID (e.g., 'ord_abc123')"
}
},
required: ["orderId"]
}
}
Create Tool
{
name: "create_support_ticket",
description: "Create a new customer support ticket. Returns the ticket ID and status.",
inputSchema: {
type: "object",
properties: {
subject: {
type: "string",
description: "Brief summary of the issue"
},
description: {
type: "string",
description: "Detailed description of the problem"
},
priority: {
type: "string",
enum: ["low", "normal", "high", "urgent"],
default: "normal"
},
category: {
type: "string",
enum: ["billing", "technical", "general", "returns"]
}
},
required: ["subject", "description"]
}
}
Update Tool
{
name: "update_order_status",
description: "Change the status of an order. Use get_order first to verify current status.",
inputSchema: {
type: "object",
properties: {
orderId: {
type: "string",
description: "The order to update"
},
status: {
type: "string",
enum: ["processing", "shipped", "delivered", "cancelled"],
description: "New status"
},
note: {
type: "string",
description: "Optional note explaining the change"
}
},
required: ["orderId", "status"]
}
}
Delete/Dangerous Tool
{
name: "cancel_order",
description: "Cancel an order and initiate refund if applicable. This action cannot be undone. The customer will be notified by email.",
inputSchema: {
type: "object",
properties: {
orderId: {
type: "string",
description: "The order to cancel"
},
reason: {
type: "string",
enum: ["customer_request", "out_of_stock", "fraud", "other"],
description: "Reason for cancellation"
},
refundMethod: {
type: "string",
enum: ["original_payment", "store_credit"],
default: "original_payment"
}
},
required: ["orderId", "reason"]
},
// Diosc extension - hint for approval policies
metadata: {
dangerous: true,
requiresApproval: true
}
}
Error Handling
Provide Helpful Error Messages
async function executeToolCall(name: string, args: any, auth: Record<string, string>) {
try {
const result = await callApi(name, args, auth);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
return formatError(error);
}
}
function formatError(error: any) {
// Don't expose internal errors
if (error.message.includes('ECONNREFUSED')) {
return {
isError: true,
content: [{ type: "text", text: "Service temporarily unavailable. Please try again." }]
};
}
// Provide helpful messages for common errors
if (error.status === 404) {
return {
isError: true,
content: [{ type: "text", text: "Item not found. Please verify the ID and try again." }]
};
}
if (error.status === 403) {
return {
isError: true,
content: [{ type: "text", text: "You don't have permission to perform this action." }]
};
}
// Generic fallback
return {
isError: true,
content: [{ type: "text", text: `An error occurred: ${error.message}` }]
};
}
Testing Tools
Manual Testing
# Test tool discovery
curl http://localhost:3000/tools | jq '.tools[] | .name'
# Test tool call
curl -X POST http://localhost:3000/call \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"name": "search_orders",
"arguments": {"status": "pending"}
}'
Automated Tests
describe('search_orders tool', () => {
it('returns orders matching status', async () => {
const result = await callTool('search_orders', {
status: 'pending'
}, mockAuth);
expect(result.isError).toBeFalsy();
expect(JSON.parse(result.content[0].text)).toHaveLength(3);
});
it('returns error for invalid status', async () => {
const result = await callTool('search_orders', {
status: 'invalid'
}, mockAuth);
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('Invalid status');
});
it('handles API errors gracefully', async () => {
mockApi.mockRejectedValue(new Error('Connection failed'));
const result = await callTool('search_orders', {}, mockAuth);
expect(result.isError).toBeTruthy();
expect(result.content[0].text).toContain('temporarily unavailable');
});
});
Next Steps
- Testing MCP Servers - Comprehensive testing guide
- Diosc Requirements - Server requirements
- Admin Portal: MCP Servers - Registering your server