Extending Copilot in Visual Studio Code

25 minute read

In this tutorial I will explain how to extend Visual Studio Code with a customized AI experience. By creating a Visual Studio Code Copilot Extension, we will contribute tools, MCP servers and chat participants, to provide AI features with domain expert know-how.

Obviously we will use and extend Copilot to achieve this. There are also other Visual Studio Code extensions to integrate AI in Visual Studio Code, but Copilot is the most prominent integration and there are several tutorials available I will refer to. This way it is easier to get started with those kind of extensions.

The article AI extensibility in VS Code gives an overview of the AI extensibility options in Visual Studio Code. In this tutorial we will

  • create a custom Language Model Tool
  • configure and contribute MCP Server
  • create a custom Chat Participant

In first place this tutorial is about extending GitHub Copilot in Visual Studio code and not a tutorial about using it. If you are interested in that, have a look at Get started with GitHub Copilot in VS Code.

Prerequisites

To follow this tutorial you need the following tools and services:

Project Setup

As this tutorial is part of my Visual Studio Code Extension - Theia - Cookbook, I will use the Dev Container created with the Getting Started with Visual Studio Code Extension Development.

Note:
If you are familiar with setting up a new Visual Studio Code Extension project or you don’t want to use the Cookbook sources and the Dev Container setup defined there as a starting point, you can create the project setup yourself, skip the following section and directly move on to Language Model Tool.

  • Clone the Visual Studio Code Extension - Theia - Cookbook GitHub Repository from the theia_getting_started branch. You can also use the getting_started branch if you are not interested in the Eclipse Theia related sources in the repository.
    git clone -b theia_getting_started https://github.com/fipro78/vscode_theia_cookbook.git
    
  • Switch to the new folder and open Visual Studio Code
    cd vscode_theia_cookbook
    code .
    
  • When asked, select Reopen in Container to build and open the project in the Dev Container

Create the Visual Studio Code Extension project

Create a new Visual Studio Code Extension project:

  • Open a Terminal and execute the following command

    yo code
    
  • Answer the questions of the wizard for example like shown below:

    # ? What type of extension do you want to create? New Extension (TypeScript)
    # ? What's the name of your extension? copilot-extension
    # ? What's the identifier of your extension? copilot-extension
    # ? What's the description of your extension? LEAVE BLANK
    # ? Initialize a git repository? No
    # ? Which bundler to use? unbundled
    # ? Which package manager to use? npm
    
    # ? Do you want to open the new folder with Visual Studio Code? Skip
    

A new subfolder copilot-extension will be created that contains the sources of the Visual Studio Code Extension.

The cookbook repository is setup as a mono-repo, therefore the Visual Studio Code Extension is kept in a subfolder. The following modifications are needed to include the newly created extension project to the setup:

  • Edit the .vscode/tasks.json

    • Add a task for watching the new copilot extension
    {
      "label": "Copilot Extension Watch",
      "type": "shell",
      "command": "npm run watch",
      "problemMatcher": "$tsc-watch",
      "isBackground": true,
      "presentation": {
        "reveal": "never"
      },
      "group": {
        "kind": "build"
      },
      "options": {
        "cwd": "${workspaceFolder}/copilot-extension"
      }
    },
    
    • Update the default build task Watch Extensions and add the new Copilot Extension Watch to the dependsOn configuration
    {
      "label": "Watch Extensions",
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "dependsOn": [
        "VS Code Extension Watch",
        "Angular Extension Watch",
        "React Extension Watch",
        "Copilot Extension Watch"
      ]
    },
    
  • Edit the .vscode/launch.json and add the new project folder to the extensionDevelopmentPath and the outFiles

    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}/vscode-extension",
        "--extensionDevelopmentPath=${workspaceFolder}/angular-extension",
        "--extensionDevelopmentPath=${workspaceFolder}/react-extension",
        "--extensionDevelopmentPath=${workspaceFolder}/copilot-extension"
      ],
      "outFiles": [
        "${workspaceFolder}/vscode-extension/out/**/*.js",
        "${workspaceFolder}/angular-extension/dist/**/*.js",
        "${workspaceFolder}/react-extension/dist/**/*.js",
        "${workspaceFolder}/copilot-extension/dist/**/*.js"
      ],
      "preLaunchTask": "${defaultBuildTask}",
      "postDebugTask": "Terminate Tasks"
    },
    

    You can also add a new run configuration that only starts the new extension if you want to focus on the new extension:

    {
      "name": "Run Copilot Extension",
      "type": "extensionHost",
      "request": "launch",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}/copilot-extension"
      ],
      "outFiles": ["${workspaceFolder}/copilot-extension/out/**/*.js"],
      "preLaunchTask": "Copilot Extension Watch",
      "postDebugTask": "Terminate Tasks"
    },
    

Note:
If you are not starting from a plain project setup and not using the Cookbook GitHub Repository, the postDebugTask will fail as it does not exist. In that case add it to the tasks.json as explained in Automatic Termination of Watch Tasks.

  • Delete the copilot-extension/.vscode folder

    rm -rf copilot-extension/.vscode
    

To verify that the setup works, open the file copilot-extension/src/extension.ts and press F5 to start a new Visual Studio Code instance with the extension, open the Command Palette (CTRL + SHIFT + P) and search for Hello to run the command.

If you only want to start the new copilot-extension in the Extension Host, switch first to the Run and Debug view (CTRL + SHIFT + D) and select Run Copilot Extension in the dropdown.

Language Model Tool

Adding a Language Model Tool enables you to extend the functionality of a large language model (LLM) in the chat with domain-specific capabilities. This is also possible via specialized MCP Server which will be described later. The main difference between a Language Model Tool and a MCP Server is that a Language Model Tool can deeply integrate with Visual Studio Code by using the VS Code APIs, while MCP Server provide access to external tools that don’t need to access to the VS Code API.

Further details can be found in Language Model Tool API.

In this section we will create a Language Model Tool that creates a file in the current workspace that contains a joke as content.

Note:
Actually Visual Studio Code already contains built-in language model tools that are able to interact with the workspace. So the following implementation is not necessary to achieve the result. It is intended as an example how the implementation of a Language Model Tool could look like.

  • Open the file copilot-extension/package.json

    • Replace the contributes section with the following snippet:

      "contributes": {
        "languageModelTools": [
          {
            "name": "chat-tools-joke",
            "displayName": "Joke File Creator",
            "toolReferenceName": "jokeFileCreator",
            "canBeReferencedInPrompt": true,
            "icon": "$(files)",
            "userDescription": "Create a file that contains a joke.",
            "modelDescription": "Create a file at the given path that contains a joke.",
            "inputSchema": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string",
                  "description": "The name of the folder in the workspace where the joke file should be created."
                },
                "filename": {
                  "type": "string",
                  "description": "The name of the jokefile that should be created."
                },
                "joke": {
                  "type": "string",
                  "description": "The joke content to be written in the joke file."
                }
              }
            }
          }
        ]
      },
      
  • Create a new file copilot-extension/src/joke-file-creator.ts

    • Import the VS Code API

      import * as vscode from "vscode";
      
    • Define an interface for the tool parameters that matches the inputSchema in the package.json

      interface IJokeFileParameters {
        path: string;
        filename: string;
        joke: string;
      }
      
    • Create a class that implements vscode.LanguageModelTool

      export class JokeFileCreatorTool
        implements vscode.LanguageModelTool<IJokeFileParameters> {}
      
      • Add the following prepareInvocation() method to provide tool configuration messages.
      prepareInvocation?(
          options: vscode.LanguageModelToolInvocationPrepareOptions<IJokeFileParameters>,
          token: vscode.CancellationToken
      ): vscode.ProviderResult<vscode.PreparedToolInvocation> {
          const confirmationMessages = {
          title: "Create a joke file",
          message: new vscode.MarkdownString(
              (options.input.path !== undefined && options.input.path !== "")
              ? `Create a joke file in ${options.input.path}?`
              : "Create a joke file in the workspace root?"
          ),
          };
      
          return {
          invocationMessage: "Create a joke file",
          confirmationMessages,
          };
      }
      

      Note:
      If you return undefined, the generic confirmation message will be shown.

      • Add the following invoke() method which is called when the language model tool is invoked while processing a chat prompt.
      async invoke(
          options: vscode.LanguageModelToolInvocationOptions<IJokeFileParameters>,
          token: vscode.CancellationToken
      ): Promise<vscode.LanguageModelToolResult | null | undefined> {
          const params = options.input;
          const result = await this.createJokeFile(
              params.path,
              params.filename,
              params.joke
          );
          if (result.length > 0) {
              return new vscode.LanguageModelToolResult([
                  new vscode.LanguageModelTextPart(result),
          ]);
          } else {
              return new vscode.LanguageModelToolResult([
                  new vscode.LanguageModelTextPart(`Joke file creation failed`),
          ]);
          }
      }
      
      public async createJokeFile(
          path: string,
          filename: string,
          jokeContent: string
      ): Promise<string> {
          const workspaceFolders = vscode.workspace.workspaceFolders;
          if (!workspaceFolders) {
              vscode.window.showErrorMessage("No workspace folder open.");
              return "";
          }
      
          let folder = workspaceFolders[0];
          let pathUri = vscode.Uri.joinPath(folder.uri, path);
      
          try {
              await vscode.workspace.fs.stat(pathUri);
          } catch {
              vscode.workspace.fs.createDirectory(pathUri);
          }
      
          const fileUri = vscode.Uri.joinPath(pathUri, filename);
      
          try {
              await vscode.workspace.fs.writeFile(
                  fileUri,
                  Buffer.from(jokeContent, "utf8")
              );
              return `Joke file "${fileUri}" created!`;
          } catch (error) {
              return `Failed to create joke file: ${error}`;
          }
      }
      
    • Change copilot-extension/src/extension.ts

      • Replace the existing example code with the following snippet
      // The module 'vscode' contains the VS Code extensibility API
      // Import the module and reference it with the alias vscode in your code below
      import * as vscode from "vscode";
      import { JokeFileCreatorTool } from "./joke-file-creator";
      
      // This method is called when your extension is activated
      // Your extension is activated the very first time the command is executed
      export function activate(context: vscode.ExtensionContext) {
        // This line of code will only be executed once when your extension is activated
        console.log(
          'Congratulations, your extension "copilot-extension" is now active!'
        );
      
        // Register our custom joke creator language model tool
        // Use the name property of the tool configured in the package.json
        context.subscriptions.push(
          vscode.lm.registerTool("chat-tools-joke", new JokeFileCreatorTool())
        );
      }
      
      // This method is called when your extension is deactivated
      export function deactivate() {}
      

If everything is correctly in place, you can verify the Language Model Tool like this:

  • You need an open workspace to make the Language Model Tool work. To be able to open a workspace in the Extension Host create a folder example in the home directory of the node user in the Dev Container
    mkdir ~/example
    
  • Press F5 to start a new Visual Studio Code instance with the extension
  • Open a folder via File -> Open Folder… and select the created example folder
  • Open the Copilot Chat Editor if it is not open yet
  • Enter the following input to the Copilot Chat to execute the contributed Language Model Tool

    #jokeFileCreator create a file that contains a joke in the folder test
    
  • In the chat you will see that a prompt comes up that asks you whether to Allow the execution of the tool or if you want to Skip it. Click on Allow or even select for example Allow in this Workspace from the dropdown so you don’t need to allow the execution of the tool in the future.

  • Check the Input and Output of the tool in the chat and ensure that the file was created in the correct place with the desired content.

MCP Server

MCP, or Model Context Protocol, is an open protocol to standardize how AI applications connect with external tools and data sources. MCP servers can offer resources, prompts and tools that can be used by a client.

In this section we will cover how to add MCP servers to Visual Studio Code and the usage of tools.

There are basically two types of MCP servers:

  • Local MCP servers (stdio transport)
    A local MCP server runs on the same machine as the MCP client and is used to access local resources like files or run local scripts. Local servers are essential for tasks that require accessing local files or data that is not available remotely.
  • Remote MCP servers (streamable HTTP or server-sent events)

There are several ways to add MCP servers to Visual Studio Code:

  • Directly install them e.g. via a website like MCP Servers for agent mode
    This will create a mcp.json file in C:\Users\<NT_USER>\AppData\Roaming\Code\User with the configuration of the MCP server
  • Via mcp.json server configuration file, e.g. in the workspace in .vscode/mcp.json
  • Programmatically via a Visual Studio Code Extension

It is also possible to configure MCP servers in a Dev Container via devcontainer.json. This is described in Dev Container support.

In the following section I will describe how to manually configure MCP servers via mcp.json and programmatically via Visual Studio Code extension.

We will install

Add MCP server via mcp.json

In the following section we will add MCP servers via mcp.json file. This can be done in the Visual Studio Code instance that you use to follow this tutorial (not the Extension Host for running the created extension).

Local MCP Server

  • Create a new file .vscode/mcp.json
  • Add the following content to configure the filesystem server as local MCP server.

    {
      "servers": {
        "filesystem": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@modelcontextprotocol/server-filesystem"]
        }
      },
      "inputs": []
    }
    

Note:
The Filesystem MCP Server supports Roots and Visual Studio Code sets the roots to the workspace directory. You can set additional allowed directories via args, but they will be replaced by the roots provided by Visual Studio Code. You can therefore skip setting allowed directories as they will be replaced automatically with the workspace folder.

  • In the editor you will see actions provided as CodeLens that let you interact with the server. Click on Start to start the filesystem MCP server.

    This will start the server and discover the capabilities and tools provided by the server. These tools can then be used in the Copilot Chat in agent mode.

  • Test if the configuration works

    • Ensure to have the Agent mode enabled.
    • Enter the following to the Copilot Chat

      list allowed directories
      
    • When asked if the tool list_allowed_directories should be executed, select Allow
    • You should now see that the list_allowed_directories tool from the filesystem (MCP Server) is executed to solve your request.

Remote MCP Server

  • Add the following content to the mcp.json to configure the fetch server as remote MCP server.
    "fetch": {
      "url": "https://remote.mcpservers.org/fetch/mcp",
      "type": "http"
    }
    

    Note:
    Visual Studio Code already provides fetch as a built-in tool. So this is actually not needed for usage, but an example to show a simple remote MCP server configuration. For testing that the added remote fetch MCP server works

    • Click on Configure Tools…
    • Disable the built-in fetch tool
    • Enable the added MCP server fetch in the configuration
    • Click on OK to apply the changes
  • Start the fetch MCP server via Codelens
  • Test if the configuration works by entering the following to the Copilot Chat
    fetch the content from https://eclipse.dev/nattable
    
  • When asked if the tool fetch - fetch (MCP Server) should be executed, select Allow
  • You should now see that the fetch tool from the fetch (MCP Server) is executed to solve your request.

Remote MCP Server with authorization

Most of the remote MCP servers require an authorization in order to work. Although of course possible, you typically don’t want to write the token in plain text into the mcp.json file, as you don’t want to share your personal token with other project members. Instead you can deal with the authorization token the following ways:

  • use input variables
  • use environment variables

To demonstrate this we use the GitHub MCP Server with a PAT (Personal Access Token). This is also described in Using the GitHub MCP Server.

You can use the GitHub MCP server locally via Docker. To make this work in a Dev Container you need the docker-in-docker feature added to the devcontainer.json.

"features": {
  "ghcr.io/devcontainers/features/docker-in-docker:2": {},
}

The local GitHub MCP server can then be configured in the mcp.json file like this:

"github": {
  "command": "docker",
  "args": [
    "run",
    "-i",
    "--rm",
    "-e",
    "GITHUB_PERSONAL_ACCESS_TOKEN",
    "ghcr.io/github/github-mcp-server"
  ],
  "env": {
    "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
  }
}

Note the ${input:github_token} in the env section. The input parameter needs to be configured in the following way in the inputs section of the mcp.json file.

"inputs": [
  {
    "type": "promptString",
    "id": "github_token",
    "description": "GitHub Personal Access Token",
    "password": true
  }
]

Inputs will be prompted on first server start and then stored securely by Visual Studio Code.

Further information about input variables can be found here:

Instead of using the GitHub MCP server locally, you can directly use the GitHub hosted server as explained in A practical guide on how to use the GitHub MCP server

"github": {
  "url": "https://api.githubcopilot.com/mcp/",
  "type": "http",
  "headers": {
    "Authorization": "Bearer ${input:github_token}"
  }
}

Instead of using an input, you can also use an environment variable as explained in Environment variables

"github": {
  "url": "https://api.githubcopilot.com/mcp/",
  "type": "http",
  "headers": {
    "Authorization": "Bearer ${env:GITHUB_TOKEN}"
  }
}

By using ${env:GITHUB_TOKEN} instead of the input variable the environment variable with the name GITHUB_TOKEN will be used for setting the authorization token.

Once the environment variable is set in your system and available, update the .devcontainer/devcontainer.json file and add the following configuration as described in Visual Studio Code - Environment variables

"remoteEnv": {
  "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}"
},

Instead of using the Bearer token for authorization, GitHub recommends the usage of OAuth for the GitHub MCP server. In that case simply drop the headers section and you will be asked for the authentication on starting the server.

"github": {
  "url": "https://api.githubcopilot.com/mcp/",
  "type": "http"
}

As the GitHub MCP Server provides a huge set of tools, the tools are categorized and not all tools are enabled by default. If you for example want to use the GitHub Gist related tools, you need to enable that. This can either be done by using the explicit MCP URL

"github_gists": {
  "url": "https://api.githubcopilot.com/mcp/x/gists",
  "type": "http"
}

or by setting the optional header X-MCP-Toolsets

"github": {
  "url": "https://api.githubcopilot.com/mcp/",
  "type": "http",
  "headers": {
    "X-MCP-Toolsets": "gists"
  }
}

Further details about this are available in Remote GitHub MCP Server.

  • Test if the GitHub MCP Server configuration works
    • Start the server
    • Ensure to have the Agent mode activated in the chat
    • Enter the following in the chat
      fetch the publications written by Dirk Fauth. Inspect the gists of fipro78 and provide the links to blog posts about VS Code and Eclipse Theia in the chat that are extracted from a related gists file
      

Note:
The default .gitignore or at least the .gitignore in the cookbook repository by default ignores the whole .vscode folder and excludes the default settings JSON files. With this configuration the mcp.json file will be ignored. This makes sense to avoid that by accident configurations with hard coded authorization tokens are added to the repository. If you are sure that no private data is contained in the mcp.json file, e.g. because you use the OAuth mechanism for the GitHub MCP server, you can exclude the mcp.json file from the ignore list by adding the following line to the .gitignore in the project root:

!.vscode/mcp.json

Add MCP server programmatically via VS Code Extension

You can also register MCP server programmatically via a Visual Studio Code extension. This way you can for example bundle AI extensions like chat participants or the direct usage of the Language Model API with the registration of MCP servers that are needed for the provided functionality.

  • Open the file copilot-extension/package.json

    • Extend the contributes section with the following mcpServerDefinitionProviders

      "contributes": {
        "mcpServerDefinitionProviders": [
          {
            "id": "custom-mcp",
            "label": "Custom MCP Server Provider"
          }
        ]
      },
      
    • To ensure that the MCP servers are registered automatically, you need to configure the activationEvents accordingly, e.g. onStartupFinished. Otherwise the extension is not activated and the MCP servers are not registered. Alternatively you can of course use another activation event based on some other constraint.

      "activationEvents": [
        "onStartupFinished"
      ],
      
  • Open the file copilot-extension/src/extension.ts

    • Register the McpServerDefinitionProvider in the activate() method.

      const didChangeEmitter = new vscode.EventEmitter<void>();
      
      context.subscriptions.push(
        vscode.lm.registerMcpServerDefinitionProvider("custom-mcp", {
          onDidChangeMcpServerDefinitions: didChangeEmitter.event,
          provideMcpServerDefinitions: async () => {
            let servers: vscode.McpServerDefinition[] = [];
      
            // add the servers
      
            return servers;
          },
        })
      );
      
    • Add a local MCP server by using vscode.McpStdioServerDefinition (right after the // add the servers comment)

      servers.push(
        new vscode.McpStdioServerDefinition("filesystem", "npx", [
          "-y",
          "@modelcontextprotocol/server-filesystem",
          "/home/node/example",
        ])
      );
      
    • Add a remote MCP server by using vscode.McpHttpServerDefinition

      servers.push(
        new vscode.McpHttpServerDefinition(
          "fetch",
          vscode.Uri.parse("https://remote.mcpservers.org/fetch/mcp")
        )
      );
      

    There are some flaws related to programmatically registered MCP servers:

    • There is no programmatical way to autostart a MCP server.
    • They do not show up in the MCP Servers section of the Extensions view.
    • They don’t get roots set.

    The resources and tools provided by a MCP server are loaded and cached on the first start. But because of the above reasons, a programmatically registered MCP servers will not be started automatically and needs a user interaction for the first start. The only way to manage programmatically registered MCP servers manually is to use the command MCP: List Servers command from the Command Palette (F1) to view the list of configured MCP servers.

    • Command Palette (F1) -> MCP: List Servers -> select the server to start -> Start Server

    There is also a configuration Chat > MCP: Autostart available that lets a user define an autostart behavior for MCP servers.

    • Command Palette (F1) -> Preferences: Open User Settings -> search for autostart -> Chat > MCP: Autostart - set the value to newAndOutdated ("chat.mcp.autostart": "newAndOutdated" in the settings JSON).

    With this setting even programmatically registered MCP servers can be autostarted. But as for example the roots are not set by Visual Studio Code to programmatically registered MCP servers, the filesystem MCP server can be configured to access an directory outside the workspace via the allowed directories parameter, in the above configuration /home/node/example.

  • Run the extension by pressing F5

    • Check that the MCP servers are started, either manually via command palette or have the chat.mcp.autostart setting enabled.
    • Test if the programmatically registered MCP server works, e.g. by entering the following into the chat
      show me the directory tree of the allowed directories
      

If you need to pass an authorization token via header like the PAT for the GitHub MCP server, you can for example read an environment variable or hard-code it and pass the Authorization header

let token = process.env.GITHUB_TOKEN;
servers.push(
  new vscode.McpHttpServerDefinition(
    "github",
    vscode.Uri.parse("https://api.githubcopilot.com/mcp/"),
    {
      Authorization: `Bearer ${token}`,
    }
  )
);

Alternatively it is possible to get the token interactively by implementing resolveMcpServerDefinition() of the McpServerDefinitionProvider

export function activate(context: vscode.ExtensionContext) {
  const didChangeEmitter = new vscode.EventEmitter<void>();

  context.subscriptions.push(
    vscode.lm.registerMcpServerDefinitionProvider("etas-mcp", {
      onDidChangeMcpServerDefinitions: didChangeEmitter.event,
      provideMcpServerDefinitions: async () => {
        let servers: vscode.McpServerDefinition[] = [];

        // add the servers
        servers.push(
          new vscode.McpStdioServerDefinition("filesystem", "npx", [
            "-y",
            "@modelcontextprotocol/server-filesystem",
            "/home/node/example",
          ])
        );

        servers.push(
          new vscode.McpHttpServerDefinition(
            "fetch",
            vscode.Uri.parse("https://remote.mcpservers.org/fetch/mcp")
          )
        );

        servers.push(
          new vscode.McpHttpServerDefinition(
            "github",
            vscode.Uri.parse("https://api.githubcopilot.com/mcp/")
          )
        );

        return servers;
      },
      resolveMcpServerDefinition: async (
        server: vscode.McpServerDefinition
      ) => {
        if (server.label === "github") {
          // First check if token is available in environment variable
          let token = process.env.GITHUB_TOKEN;

          // If no environment variable, ask for token from user
          if (!token) {
            token = await vscode.window.showInputBox({
              prompt: `Enter the authorization token for ${server.label}`,
              password: true,
              placeHolder: `Enter your authorization token for ${server.label} ...`,
            });

            if (token) {
              // If user provided a token, save it to environment variable for future use
              process.env.GITHUB_TOKEN = token;
              console.log(`GITHUB_TOKEN saved to environment for future use`);
            }
          } else {
            console.log(
              `Using GITHUB_TOKEN from environment for ${server.label}`
            );
          }

          if (!token) {
            vscode.window.showErrorMessage(
              `Authorization token is required for ${server.label}`
            );
            return undefined; // Don't start the server without a token
          }

          // Update the server headers with the new token
          const updatedHeaders = {
            ...(server as any).headers,
            Authorization: `Bearer ${token}`,
          };

          (server as any).headers = updatedHeaders;
        }

        // Return undefined to indicate that the server should not be started or throw an error
        // If there is a pending tool call, the editor will cancel it and return an error message
        // to the language model.
        return server;
      },
    })
  );
}

Additional information about registering a MCP server programmatically is also provided here:

Note:
If you plan to use a Visual Studio Code extension that contributes mcpServerDefinitionProviders in a Theia application, you can not use a type check for vscode types, e.g. the statement if (server instanceof vscode.McpHttpServerDefinition) will not work in Theia, because Theia does not use the vscode types. In such a case you need a different check like if ((server as any).uri) which checks for the property instead of the type, or the name check as shown in the above example.

Further information about MCP in Visual Studio Code:

Chat Participant

With the implementation of a Chat Participant you can create an assistant to extend the chat with domain specific experts knowledge.

Chat Participants

  • are specialized AI assistants
  • work within VS Code’s chat system and can interact with VS Code APIs
  • can be distributed and deployed with an extension via marketplace, no need for additional installation mechanisms
  • can control the end-to-end user chat prompt and response

In the following section we will create a chat participant that is able to use one of the tools we created before.

We use the chat-extension-utils to implement tool calling in the chat participant. You can also implement the tool calling yourself to have more control over the tool calling process. Check the tool calling by using prompt-tsx example for an example on how to implement the tool calling yourself.

  • Install @vscode/chat-extension-utils to use tools in the chat participant

    • Open a Terminal
    • Switch to the folder copilot-extension
    • Execute the following command
      npm install @vscode/chat-extension-utils
      
  • Open the file copilot-extension/package.json

    • Extend the contributes section with the following chatParticipants

      "contributes": {
        "chatParticipants": [
          {
            "id": "joker-sample.joker-participant",
            "name": "joker",
            "fullName": "The Joker",
            "description": "Let me tell you a joke!",
            "isSticky": true
          }
        ]
      },
      
  • Open the file copilot-extension/src/extension.ts

    • Define the prompt to use

      const JOKER_PROMPT = `You are the Joker, the arch enemy of Batman.
      To attack Batman, you tell a joke that is so funny, it distracts him from his mission.
      To keep the distraction going on, write the joke to a file.
      If the user does not provide a path, create a new folder "bat-jokes" in the current workspace folder and store the file in that folder.`;
      
    • Import chat-extension-utils

      import * as chatUtils from "@vscode/chat-extension-utils";
      
  • Extend the activate() method

    • Define the vscode.ChatRequestHandler request handler

      const handler: vscode.ChatRequestHandler = async (
        request: vscode.ChatRequest,
        context: vscode.ChatContext,
        stream: vscode.ChatResponseStream,
        token: vscode.CancellationToken
      ) => {
        // Chat request handler implementation goes here
        try {
          const tools = vscode.lm.tools.filter(
            (tool) => tool.name === "chat-tools-joke"
          );
      
          const libResult = chatUtils.sendChatParticipantRequest(
            request,
            context,
            {
              prompt: JOKER_PROMPT,
              responseStreamOptions: {
                stream,
                references: true,
                responseText: true,
              },
              tools,
            },
            token
          );
      
          return await libResult.result;
        } catch (err) {
          console.log(`Error in chat request handler: ${err}`);
        }
      };
      
    • Register the chat participant

      const chatLibParticipant = vscode.chat.createChatParticipant(
        "joker-sample.joker-participant",
        handler
      );
      context.subscriptions.push(chatLibParticipant);
      
    • Test the implementation by starting the extension host via F5.

      • Open the Copilot Chat and ask the joker chat participant for a joke by selecting him via @joker

        @joker tell me a joke about batman
        
      • Verify the chat response and check the generated file

Further information about chat participants can be found here

Further Customizations

Users can further customize the Copilot experience by configuring Instructions, Prompt Templates and Chat Modes.

  • Instructions
    Custom Instructions are used to define common guidelines for specific tasks and can be installed in the user profile or in the workspace .github/instructions
  • Prompts
    Prompt files are used to define reusable prompts and can be installed in the user profile or in the .github/prompts folder
  • Chat Modes
    Chat Modes are used to create a specialist assistant for specific tasks can be installed in the user profile or in the workspace in the .github/chatmodes folder
  • Tool Sets
    Tool Sets can be defined in a .jsonc file that is located in the user profile e.g. C:\Users\<username>\AppData\Roaming\Code\User\prompts

By having the instructions, prompts and chat-modes in the workspace, it is possible to have dedicated instructions, prompts and chat-modes per project that are checked in the repository.

Further information can be found in Customize chat to your workflow.

I will not go into details of every possible customization. But as an example and comparison to the previous programmatically registered Chat Participant, we will create a prompt that is able to achieve the same.

  • Start the Extension Host by pressing F5
    This is necessary because the jokeFileCreator language model tool is contributed by the developed Visual Studio Code Extension. If that tool is not needed or used, you can even try this in the Visual Studio Code instance in which you are developing.

  • Create a new prompt
    In the Copilot chat window, click the gear icon in the upper right corner (Configure Chat…) and select
    Prompt Files -> New Prompt File -> .github/prompts -> name: joker

  • Add the following content to the file

    ---
    mode: agent
    tools: ['undefined_publisher.copilot-extension/jokeFileCreator']
    ---
    
    You are the Joker, the arch enemy of Batman.
    To attack Batman, you tell a joke that is so funny, it distracts him from his mission.
    To keep the distraction going on, write the joke to a file.
    If the user does not provide a path, create a new folder "bat-jokes" in the current workspace folder and store the file in that folder.
    
  • Execute the prompt by pressing the play button in the editor title area.

  • Execute the prompt by using it in the chat via slash command and pass additional information, e.g. /joker joke about robin

Note:
As you might notice, the results from the implemented Chat Participant are quite similar to those from the Prompt File. For this simple example that uses a Language Model Tool for file creation, there is no significant advantage to creating a Chat Participant, except for the ability to distribute it via a Visual Studio Code Extension. The real benefits of a Chat Participant become apparent when you need to deeply integrate with Visual Studio Code using the extension APIs.

Conclusion

In this comprehensive tutorial, we explored the powerful extensibility options available for integrating AI capabilities into Visual Studio Code through Copilot extensions. We covered three main approaches to extend Copilot’s functionality:

Language Model Tools provide deep integration with VS Code APIs, allowing you to create domain-specific tools that can interact directly with the workspace and file system. While VS Code already includes many built-in tools, custom Language Model Tools enable specialized functionality tailored to your specific development needs.

MCP (Model Context Protocol) Servers offer a standardized way to connect AI applications with external tools and data sources. We demonstrated both manual configuration via mcp.json files and programmatic registration through VS Code extensions, covering local servers, remote servers, and authentication scenarios.

Chat Participants create specialized AI assistants that can leverage both Language Model Tools and MCP servers while providing a customized conversational interface. They excel when deep VS Code integration is required and can be easily distributed through the VS Code marketplace.

We also explored alternative customization approaches like Instructions, Prompt Files and Chat Modes, which can help to improve the Copilot experience according to your needs.

The key takeaway is that the choice between these approaches depends on your specific requirements: use Language Model Tools for VS Code-specific integrations, MCP servers for external tool access, and Chat Participants when you need a complete custom AI assistant experience that can be distributed as an extension.

The complete source code and examples from this tutorial are available in my GitHub repository, providing a practical foundation for building your own AI-powered VS Code extensions.

Whether you’re looking to enhance developer productivity with domain-specific AI tools or create entirely new AI-powered development workflows, this tutorial provides the essential building blocks to get started with extending Copilot in Visual Studio Code.

Updated: