Skip to main content

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