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

> Set up local Copilot SDK MCP servers using stdio or HTTP. Learn to implement automatic tool filtering with the allowed-tool list for enhanced control.

- Repository: [GitHub/copilot-sdk](https://github.com/github/copilot-sdk)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/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:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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.