Deep Dive: Tools
Master the art of extending Copilot with custom tools.
Why This Matters
Tools transform Copilot from a text generator into an agent that can act. Without tools, Copilot can only discuss; with tools, it can do.
Tool Definition API
Full schema structure
interface Tool {
name: string; // Unique identifier (snake_case recommended)
description: string; // What the tool does (Copilot reads this!)
parameters: {
type: "object";
properties: {
[paramName: string]: {
type: "string" | "number" | "boolean" | "array" | "object";
description: string;
enum?: string[]; // Limit to specific values
items?: { type: string }; // For arrays
default?: any; // Default value
};
};
required?: string[]; // Which params are mandatory
};
}
Parameter types
String with validation:
{
type: "string",
description: "Email address",
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
}
Number with constraints:
{
type: "number",
description: "Quantity to order",
minimum: 1,
maximum: 100
}
Enum (fixed choices):
{
type: "string",
description: "Log level",
enum: ["debug", "info", "warn", "error"]
}
Array:
{
type: "array",
description: "List of user IDs to notify",
items: { type: "string" },
minItems: 1,
maxItems: 10
}
Nested object:
{
type: "object",
description: "User profile to update",
properties: {
name: { type: "string" },
age: { type: "number" }
}
}
Advanced Patterns
Pattern 1: Confirmation flow
For destructive or irreversible actions:
const deleteUserTool = {
name: "delete_user",
description: "Permanently delete a user account. REQUIRES explicit user confirmation.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID to delete" },
confirmedByUser: {
type: "boolean",
description: "Set to true ONLY after user explicitly confirms deletion"
}
},
required: ["userId", "confirmedByUser"]
}
};
function handleDeleteUser({ userId, confirmedByUser }) {
if (!confirmedByUser) {
return {
status: "pending_confirmation",
message: `Are you sure you want to delete user ${userId}? This cannot be undone.`
};
}
// Actually delete
return { status: "deleted", userId };
}
Pattern 2: Pagination
For tools that return large result sets:
const searchTool = {
name: "search_documents",
description: "Search documents. Returns paginated results. Use 'cursor' from previous results to get more.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search query" },
limit: {
type: "number",
description: "Results per page (default 10, max 50)",
default: 10
},
cursor: {
type: "string",
description: "Pagination cursor from previous results"
}
},
required: ["query"]
}
};
function handleSearch({ query, limit = 10, cursor }) {
const results = database.search(query, { limit: limit + 1, after: cursor });
const hasMore = results.length > limit;
return {
results: results.slice(0, limit),
nextCursor: hasMore ? results[limit - 1].id : null,
hasMore
};
}
Pattern 3: Tool chaining guidance
Guide Copilot through multi-step workflows:
const listFilesTool = {
name: "list_files",
description: "List files in a directory. Returns file names only. Use 'read_file' to get contents of specific files.",
// ...
};
const readFileTool = {
name: "read_file",
description: "Read contents of a file. Use after 'list_files' to examine specific files.",
// ...
};
const modifyFileTool = {
name: "modify_file",
description: "Modify a file. Always use 'read_file' first to see current contents before modifying.",
// ...
};
Pattern 4: Progress reporting
For long-running operations:
const generateReportTool = {
name: "generate_report",
description: "Generate a detailed report. May take time. Returns a job ID to check progress.",
parameters: {
type: "object",
properties: {
reportType: { type: "string", enum: ["daily", "weekly", "monthly"] },
format: { type: "string", enum: ["pdf", "csv", "json"] }
},
required: ["reportType"]
}
};
const checkProgressTool = {
name: "check_report_progress",
description: "Check progress of a report generation job",
parameters: {
type: "object",
properties: {
jobId: { type: "string", description: "Job ID from generate_report" }
},
required: ["jobId"]
}
};
Pattern 5: Tool with side effect warnings
const sendEmailTool = {
name: "send_email",
description: `Send an email to specified recipients.
⚠️ THIS SENDS A REAL EMAIL - verify recipient and content with user before calling.
⚠️ Cannot be undone once sent.`,
parameters: {
type: "object",
properties: {
to: { type: "array", items: { type: "string" }, description: "Recipient email addresses" },
subject: { type: "string", description: "Email subject line" },
body: { type: "string", description: "Email body (plain text)" }
},
required: ["to", "subject", "body"]
}
};
Error Handling Strategies
Strategy 1: Structured errors
interface ToolResult {
success: boolean;
data?: any;
error?: {
code: string;
message: string;
recoverable: boolean;
suggestion?: string;
};
}
function handleTool(name: string, args: any): ToolResult {
try {
// ... execute tool
return { success: true, data: result };
} catch (error) {
if (error instanceof NotFoundError) {
return {
success: false,
error: {
code: "NOT_FOUND",
message: error.message,
recoverable: true,
suggestion: "Try searching with different parameters"
}
};
}
// Re-throw unexpected errors
throw error;
}
}
Strategy 2: Retry hints
function handleApiCall(params) {
try {
return callExternalApi(params);
} catch (error) {
if (error.code === "RATE_LIMITED") {
return {
error: true,
code: "RATE_LIMITED",
retryAfterSeconds: error.retryAfter,
message: `Rate limited. Can retry in ${error.retryAfter} seconds.`
};
}
throw error;
}
}
Strategy 3: Graceful degradation
function handleWeather({ city }) {
try {
return fetchWeatherFromPrimary(city);
} catch (primaryError) {
try {
// Try backup source
const backup = fetchWeatherFromBackup(city);
return {
...backup,
note: "Data from backup source, may be less accurate"
};
} catch (backupError) {
return {
error: true,
message: "Weather service unavailable",
suggestion: "Try again later or ask about a different city"
};
}
}
}
MCP (Model Context Protocol) Integration
MCP servers provide pre-built tool sets. The SDK integrates with them seamlessly.
Using an MCP server
const session = await client.createSession({
mcpServers: [{
name: "github",
command: "npx",
args: ["@github/mcp-server"]
}]
});
// Now Copilot can use GitHub tools (create issues, PRs, etc.)
Multiple MCP servers
const session = await client.createSession({
mcpServers: [
{ name: "github", command: "npx", args: ["@github/mcp-server"] },
{ name: "filesystem", command: "npx", args: ["@modelcontextprotocol/fs"] }
],
tools: [myCustomTool] // Can mix with custom tools
});
When to use MCP vs custom tools
| Use MCP | Use Custom Tools |
|---|---|
| Standard integrations (GitHub, filesystem) | Business-specific logic |
| You want to reuse community tools | You need full control |
| Quick prototyping | Production with specific requirements |
Pitfalls
1. Vague descriptions
❌ Bad:
{ description: "Does stuff with data" }
✅ Good:
{ description: "Query the user database by email. Returns user profile or null if not found." }
2. Missing error context
❌ Bad:
return { error: true }
✅ Good:
return {
error: true,
code: "USER_NOT_FOUND",
message: "No user with email john@example.com",
suggestion: "Verify the email address or search by name instead"
}
3. Unbounded results
❌ Bad:
// Returns potentially thousands of results
function listAllUsers() {
return database.users.findAll();
}
✅ Good:
function listUsers({ limit = 20, offset = 0 }) {
return {
users: database.users.findAll().slice(offset, offset + limit),
total: database.users.count()
};
}
4. Synchronous long operations
❌ Bad:
function generateReport() {
// Blocks for 30 seconds
return slowReportGeneration();
}
✅ Good:
function startReportGeneration() {
const jobId = queue.add(slowReportGeneration);
return { jobId, status: "queued" };
}
Exercises
- Basic: Create a tool that converts between units (miles to km, etc.)
- Intermediate: Build a “memory” tool that lets Copilot save and recall information
- Advanced: Create a tool chain for managing a todo list (add, list, complete, delete) with proper confirmation for deletions
See exercises/ for starter code and solutions.