Setting up Local Copilot SDK MCP Servers with stdio and HTTP Including Tool Filtering

You can run local MCP servers using the Copilot SDK over stdio (for local processes) or HTTP (for remote containers), with automatic tool filtering controlled by the session's allowed-tool list.

The github/copilot-sdk repository provides the Model Context Protocol (MCP) implementation used by GitHub Copilot extensions to register custom tools. Setting up local MCP servers requires configuring the transport layer—either spawning a child process over stdio streams or connecting to a TCP port via HTTP—while the SDK handles tool discovery and filtering based on your extension's permissions.

MCP Server Architecture Overview

The SDK abstracts MCP server creation into two components: the core server logic and the transport layer that handles JSON-RPC communication.

The Core McpServer Class

The McpServer class (exported from @modelcontextprotocol/sdk/server/mcp.js) maintains a tool registry and session state. It declares capabilities via server.tool() and exposes them through a pluggable transport.

According to the source in test/harness/test-mcp-server.mjs (line 12), you initialize the server with metadata:

const server = new McpServer({ name: "env-echo", version: "1.0.0" });

Transport Layer Abstraction

The transport layer implements the JSON-RPC channel. The SDK supports three variants, but local development typically uses:

  • StdioServerTransport – Wraps a child_process spawned with --stdio (source: test/harness/test-mcp-server.mjs, line 29)
  • HttpServerTransport – Listens on a configurable TCP port and serves JSON-RPC over HTTP (source: test/harness/test-mcp-elicitation-server.mjs, line 48)

Both transports expose an async connect(transport) method that performs the JSON-RPC handshake and begins processing tool invocations.

Creating an MCP Server with stdio Transport

stdio is the fastest transport for local development. The SDK spawns your server as a child process and communicates via standard input/output streams.

As implemented in test/harness/test-mcp-server.mjs, a minimal stdio server requires:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "env-echo", version: "1.0.0" });

server.tool(
  "get_env",
  "Returns the value of the specified environment variable.",
  { name: z.string().describe("Environment variable name") },
  async ({ name }) => ({
    content: [{ type: "text", text: process.env[name] ?? "" }],
  })
);

await server.connect(new StdioServerTransport());

Run this with npx tsx test-mcp-server.mjs. The process now listens on STDIO and can be reached by any Copilot SDK client that includes "get_env" in its tool allow-list.

Creating an MCP Server with HTTP Transport

HTTP transport is ideal for containerized deployments or remote machines. The server listens on a specific port and accepts JSON-RPC requests over TCP.

The test harness in test/harness/test-mcp-elicitation-server.mjs demonstrates an HTTP server with tool elicitation:

import { readFile } from "fs/promises";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { HttpServerTransport } from "@modelcontextprotocol/sdk/server/http.js";

const configPath = process.argv[process.argv.indexOf("--config") + 1];
const requests = JSON.parse(await readFile(configPath, "utf-8"));

const server = new McpServer({ name: "test-elicitation-server", version: "1.0.0" });

server.registerTool("request_user_input", {
  description: "Request structured input from the user via an elicitation form",
  inputSchema: {},
}, async () => {
  const results = [];
  for (const req of requests) {
    const result = await server.server.elicitInput(req);
    results.push({ action: result.action, content: result.content });
    if (result.action !== "accept") break;
  }
  return { content: [{ type: "text", text: JSON.stringify({ results }) }] };
});

await server.connect(new HttpServerTransport({ port: 4000 }));

Start this with node test-mcp-elicitation-server.mjs --config ./requests.json. The server binds to http://localhost:4000 and can be referenced in SDK configuration as "mcpServers": { "my-http": { "type": "http", "port": 4000 } }.

Tool Registration and Filtering

Tool registration defines which functions the LLM can invoke, while filtering restricts which registered tools are actually exposed to the model during a specific session.

Declaring Tools with server.tool()

Register tools using server.tool(name, description, schema, handler):

  • name – The identifier the LLM uses to call the tool.
  • description – Human-readable context for the model.
  • schema – A Zod object describing input parameters.
  • handler – An async function returning content in the tool-result format ({ content: [{type: "text", text: ...}] }).

The example in test/harness/test-mcp-server.mjs (lines 20-27) validates parameters with Zod before processing.

How Tool Filtering Works

The SDK automatically filters the advertised tool list based on the session's tool-allow list set by the extension author. Only tools explicitly allowed appear in the model's tool list and can be invoked. This prevents unauthorized access to sensitive server capabilities.

In the Rust source code (rust/src/lib.rs, lines 1746-1754), the Client::spawn_stdio function handles process spawning, while lines 894-902 define the Transport::Stdio and Transport::Http enum variants used for transport selection.

Connecting from a Node.js Client

To consume an MCP server from your extension, use CopilotClient with RuntimeConnection configured for your transport:

import { CopilotClient, RuntimeConnection } from "@modelcontextprotocol/sdk/client";

const client = new CopilotClient({
  connection: RuntimeConnection.for_stdio({ path: "node test-mcp-server.mjs" }),
});

await client.session.create({ tools: ["get_env"] });
const result = await client.session.runTool("get_env", { name: "PATH" });
console.log(result.content[0].text); // prints the current PATH

The TypeScript definitions in nodejs/src/types.ts specify the RuntimeConnection options for both for_stdio and for_http configurations.

Summary

  • stdio transport spawns local processes via StdioServerTransport for fast development cycles (source: test/harness/test-mcp-server.mjs).
  • HTTP transport exposes servers over TCP via HttpServerTransport for containerized or remote deployments (source: test/harness/test-mcp-elicitation-server.mjs).
  • Tool registration uses server.tool() with Zod schemas for parameter validation.
  • The SDK filters available tools based on the session-specific allow-list defined in client.session.create().
  • Transport selection logic resides in rust/src/lib.rs (lines 894-902), with process spawning implemented in Client::spawn_stdio (lines 1746-1754).

Frequently Asked Questions

What is the difference between stdio and HTTP transport in Copilot SDK?

stdio transport spawns the MCP server as a child process and communicates via standard input/output streams, making it ideal for local development and testing. HTTP transport connects to a server listening on a TCP port using JSON-RPC over HTTP, which is better suited for remote machines or containerized environments. According to rust/src/lib.rs (lines 894-902), the SDK selects between these via the Transport enum variants.

How do I filter which tools are available to the LLM?

Tool filtering is automatic based on the tool-allow list passed to client.session.create({ tools: [...] }). Only tools listed in this array are advertised to the LLM and can be invoked during that session. This prevents the model from accessing sensitive tools even if they are registered on the server.

Where does the SDK handle transport selection in the source code?

Transport selection and process spawning are implemented in the Rust core at rust/src/lib.rs. The Transport enum (lines 894-902) defines the variants, while the Client::spawn_stdio method (lines 1746-1754) handles the child process creation for stdio transports. HTTP transport configuration is parsed similarly through the generated API types.

Can I run multiple MCP servers simultaneously?

Yes. The SDK supports multiple MCP server configurations in the mcpServers configuration object. Each server can use different transports (stdio or HTTP) and expose different tool sets. The client establishes separate connections to each, and tool names are namespaced to prevent collisions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →