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 MCPUse Custom Tools
Standard integrations (GitHub, filesystem)Business-specific logic
You want to reuse community toolsYou need full control
Quick prototypingProduction 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

  1. Basic: Create a tool that converts between units (miles to km, etc.)
  2. Intermediate: Build a “memory” tool that lets Copilot save and recall information
  3. 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.