Getting Started with the GitHub Copilot SDK — Part 9: MCP Servers & Custom Agents

This is Part 9, and it's the most complete demo in the series so far. The companion
code lives at
egarim/GettingStartedWithGithubCopilotSDK.
So far the model has used the tools you wrote in C#. Now we open two doors:
MCP servers bring in external tools. Custom agents give those tools a personality and a permission boundary.
Both hang off SessionConfig. Let's build up from one server to a full architecture.
MCP servers: tools from outside your process
An MCP (Model Context Protocol) server is an external tool server that speaks a
standard. You point the SDK at an executable, and the model can call whatever tools that
process exposes — no extra C# on your side.
You wire it up with the McpServers dictionary:
var session = await client.CreateSessionAsync(new SessionConfig
{
McpServers = new Dictionary<string, object>
{
["test-server"] = new McpLocalServerConfig
{
Type = "local",
Command = "echo", // any executable — npx, python, dotnet…
Args = ["hello-mcp"],
Tools = ["*"] // expose every tool the server has
}
}
});
Four properties carry the whole config: Type, Command, Args, and Tools.
Tools = ["*"]means "all of them." Swap in specific names to filter.
The session works exactly like before — the MCP server just sits there as an extension the
model reaches for when it needs to.
More than one server
A session isn't limited to one. Give each its own key and you can orchestrate several
sources at once — say a filesystem server and a database server:
McpServers = new Dictionary<string, object>
{
["filesystem"] = new McpLocalServerConfig { Type = "local", Command = "echo", Args = ["fs"], Tools = ["*"] },
["database"] = new McpLocalServerConfig { Type = "local", Command = "echo", Args = ["db"], Tools = ["*"] }
}
Each config is fully independent. One session, many tool surfaces.
Custom agents: roles with boundaries
The second door. A custom agent is a named specialist with its own system prompt:
CustomAgents = new List<CustomAgentConfig>
{
new CustomAgentConfig
{
Name = "business-analyst",
DisplayName = "Business Analyst Agent",
Description = "Specialized in business analysis",
Prompt = "You are a business analyst. Focus on KPIs and data-driven insights.",
Infer = true // let the model decide when to delegate here
}
}
Infer = true is the key flag. The model reads the Description and decides on its own
when to hand work to this agent. Set it to false and the agent only runs when you call it
explicitly.
Lock an agent down to specific tools
This is the part I care about most for production. An agent can be restricted to a named
set of tools — nothing else:
new CustomAgentConfig
{
Name = "devops-agent",
DisplayName = "DevOps Agent",
Description = "An agent for DevOps tasks",
Prompt = "You are a DevOps agent. You can use bash and edit tools.",
Tools = ["bash", "edit"], // this agent can do exactly two things
Infer = true
}
Least privilege, but for agents. Each one can only do what you explicitly allow.
Give an agent its own MCP servers
Agents can also carry private MCP connections. Servers defined inside an agent are scoped
to that agent — the main session and the other agents can't see them:
new CustomAgentConfig
{
Name = "data-agent",
Prompt = "You analyze data.",
McpServers = new Dictionary<string, object>
{
["agent-db"] = new McpLocalServerConfig { Type = "local", Command = "echo", Args = ["db"], Tools = ["*"] }
}
}
Isolation by design. A data agent's database tools stay with the data agent.
Mixing infer modes
Put several agents on one session and you control exactly how they're orchestrated:
CustomAgents = new List<CustomAgentConfig>
{
new() { Name = "frontend-agent", Prompt = "You are a frontend expert." }, // Infer defaults — model may pick it
new() { Name = "backend-agent", Prompt = "You are a backend expert.", Infer = false } // explicit invocation only
}
Some agents the model can reach for automatically; others wait until you ask by name.
Adding capabilities on resume
Here's the advanced pattern. You don't have to declare everything up front. Create a
session, talk to it, then resume it with MCP servers and agents that didn't exist
before:
var resumed = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig
{
McpServers = new Dictionary<string, object> { /* … */ },
CustomAgents = new List<CustomAgentConfig> { /* … */ }
});
That's powerful: capabilities can switch on dynamically, driven by where the
conversation has gone.
Takeaways
- MCP servers wire external tool processes into a session via the
McpServers
dictionary —Type,Command,Args,Tools. Use["*"]for all tools, or filter. - A session can hold many MCP servers, each independently keyed.
- Custom agents are named roles with a
Prompt.Infer = truelets the model delegate
automatically;falsemeans explicit-only. - Restrict an agent with
Tools = [...], and give it private MCP servers — both are
scoped to that agent. This is your security boundary. ResumeSessionConfiglets you add servers and agents to an existing session, so
capabilities can grow as the conversation does.
Heads up: the demos use
echoas a no-op command because they're showing
configuration, not a live tool call. Swap in a real MCP server binary and the tools go live.
That's the full extensibility story — external tools plus controlled, specialized agents.
In Part 10 — A Full-Stack Blazor Chat App
we put everything together into a real Blazor app you can actually chat with.