Skip to content

Commit bf1592a

Browse files
feat: add managed MCP session support
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3d630a7 commit bf1592a

59 files changed

Lines changed: 2973 additions & 81 deletions

Some content is hidden

Large 提交 have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/features/mcp.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,86 @@ The Copilot SDK can integrate with **MCP servers** (Model Context Protocol) to e
1515
* Call external APIs
1616
* And much more
1717

18-
## Server types
18+
## Server transports
1919

20-
The SDK supports two types of MCP servers:
20+
The SDK supports two MCP transport families:
2121

2222
| Type | Description | Use Case |
2323
|------|-------------|----------|
2424
| **Local/Stdio** | Runs as a subprocess, communicates via stdin/stdout | Local tools, file access, custom scripts |
2525
| **HTTP/SSE** | Remote server accessed via HTTP | Shared services, cloud-hosted tools |
2626

27+
Transport and configuration origin are separate concepts. A managed MCP server
28+
uses the HTTP transport, but its configuration comes from a trusted host catalog
29+
instead of user or workspace configuration. Session status and loaded-server
30+
events report this distinction with `source: "managed"` and include the catalog
31+
display name.
32+
33+
## Managed MCP servers
34+
35+
Managed MCP lets a trusted SDK host inject a catalog of non-secret hosted
36+
servers for one session. The runtime keeps this catalog separate from
37+
`mcpServers`, so existing local stdio, HTTP, SSE, and OAuth behavior remains
38+
unchanged.
39+
40+
The host supplies each server under a stable managed identity:
41+
42+
<!-- docs-validate: skip -->
43+
44+
```typescript
45+
const session = await client.createSession({
46+
managedMcpServers: {
47+
"github-enterprise": {
48+
displayName: "GitHub Enterprise",
49+
url: "https://mcp.example.com/",
50+
tools: ["issues", "pull_requests"],
51+
timeout: 30_000,
52+
headersRefreshTtlMs: 60_000,
53+
},
54+
},
55+
onMcpHeadersRefresh: async ({ serverName, serverUrl, reason }) => {
56+
const credential = await broker.getCredential({ serverName, serverUrl, reason });
57+
return {
58+
headers: { Authorization: credential.authorizationHeader },
59+
ttlMs: credential.expiresInMs,
60+
};
61+
},
62+
});
63+
```
64+
65+
The corresponding configuration and callback names are:
66+
67+
| SDK | Managed servers | Header refresh callback |
68+
| --- | --- | --- |
69+
| Node.js | `managedMcpServers` | `onMcpHeadersRefresh` |
70+
| Python | `managed_mcp_servers` | `on_mcp_headers_refresh` |
71+
| Go | `ManagedMCPServers` | `OnMCPHeadersRefresh` |
72+
| .NET | `ManagedMcpServers` | `OnMcpHeadersRefresh` |
73+
| Java | `setManagedMcpServers(...)` | `setOnMcpHeadersRefreshRequest(...)` |
74+
| Rust | `with_managed_mcp_servers(...)` | `with_mcp_headers_handler(...)` |
75+
76+
### Host responsibilities
77+
78+
Managed MCP hosts must enforce these boundaries:
79+
80+
* **Trusted catalog injection**: Only inject server identities, display metadata,
81+
endpoints, and tool policy from a trusted catalog. Do not treat model output
82+
or untrusted content as catalog configuration.
83+
* **Memory-only credentials**: Keep access tokens and derived authorization
84+
headers in memory. Do not place credentials in `managedMcpServers`, session
85+
history, workspace state, or persistent MCP OAuth storage.
86+
* **Expiry and revocation**: Set `ttlMs` to the remaining credential lifetime.
87+
The runtime clamps it to `headersRefreshTtlMs`. Throw from the callback when
88+
the broker denies access, fails, or reports revocation; the SDK forwards an
89+
explicit broker error without converting it to a successful empty response.
90+
* **Cold resume**: Re-supply both the managed catalog and the header refresh
91+
callback when cold-resuming a session. Managed configuration and credentials
92+
are not recovered from persisted session state.
93+
94+
Returning no result from the callback reports that no dynamic headers are
95+
available. Existing static `headers`, arbitrary HTTP servers, and MCP OAuth
96+
handlers continue to use their current behavior.
97+
2798
## Configuration
2899

29100
### Node.js / TypeScript

dotnet/src/Client.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ private CopilotSession InitializeSession(
825825
config.OnPermissionRequest,
826826
config.EnableManaged设置 is true || config.Managed设置 is not null);
827827
session.RegisterMcpAuthHandler(config.OnMcpAuthRequest);
828+
session.RegisterMcpHeadersRefreshHandler(config.OnMcpHeadersRefresh);
828829
session.RegisterCommands(config.Commands);
829830
session.RegisterElicitationHandler(config.OnElicitationRequest);
830831
session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest);
@@ -1206,6 +1207,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
12061207
config.Streaming is true ? true : null,
12071208
config.IncludeSubAgentStreamingEvents,
12081209
config.McpServers,
1210+
config.ManagedMcpServers,
12091211
config.McpOAuthTokenStorage,
12101212
"direct",
12111213
config.CustomAgents,
@@ -1307,6 +1309,10 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
13071309
{
13081310
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
13091311
}
1312+
if (config.OnMcpHeadersRefresh is not null)
1313+
{
1314+
await session.Rpc.EventLog.RegisterInterestAsync("mcp.headers_refresh_required", cancellationToken);
1315+
}
13101316

13111317
session.WorkspacePath = response.WorkspacePath;
13121318
session.SetCapabilities(response.Capabilities);
@@ -1456,6 +1462,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
14561462
config.Streaming is true ? true : null,
14571463
config.IncludeSubAgentStreamingEvents,
14581464
config.McpServers,
1465+
config.ManagedMcpServers,
14591466
config.McpOAuthTokenStorage,
14601467
"direct",
14611468
config.CustomAgents,
@@ -1514,6 +1521,10 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
15141521
{
15151522
await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken);
15161523
}
1524+
if (config.OnMcpHeadersRefresh is not null)
1525+
{
1526+
await session.Rpc.EventLog.RegisterInterestAsync("mcp.headers_refresh_required", cancellationToken);
1527+
}
15171528

15181529
await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false);
15191530
if (registrationId is not null)
@@ -2884,6 +2895,7 @@ internal record CreateSessionRequest(
28842895
bool? Streaming,
28852896
bool? IncludeSubAgentStreamingEvents,
28862897
IDictionary<string, McpServerConfig>? McpServers,
2898+
[property: JsonPropertyName("managedMcpServers")] IDictionary<string, ManagedMcpServerConfig>? ManagedMcpServers,
28872899
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
28882900
string? EnvValueMode,
28892901
IList<CustomAgentConfig>? CustomAgents,
@@ -3011,6 +3023,7 @@ internal record ResumeSessionRequest(
30113023
bool? Streaming,
30123024
bool? IncludeSubAgentStreamingEvents,
30133025
IDictionary<string, McpServerConfig>? McpServers,
3026+
[property: JsonPropertyName("managedMcpServers")] IDictionary<string, ManagedMcpServerConfig>? ManagedMcpServers,
30143027
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
30153028
string? EnvValueMode,
30163029
IList<CustomAgentConfig>? CustomAgents,

dotnet/src/Generated/Rpc.cs

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Generated/SessionEvents.cs

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/src/Session.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public sealed partial class CopilotSession : IAsyncDisposable
6565
private volatile Func<PermissionRequest, PermissionInvocation, Task<PermissionDecision>>? _permissionHandler;
6666
private bool _managed设置Enabled;
6767
private volatile Func<McpAuthContext, Task<McpAuthResult?>>? _mcpAuthHandler;
68+
private volatile Func<McpHeadersRefreshContext, Task<McpHeadersRefreshResult?>>? _mcpHeadersRefreshHandler;
6869
private volatile Func<UserInputRequest, UserInputInvocation, Task<UserInputResponse>>? _userInputHandler;
6970
private volatile Func<ElicitationContext, Task<ElicitationResult>>? _elicitationHandler;
7071
private volatile Func<ExitPlanModeRequest, ExitPlanModeInvocation, Task<ExitPlanModeResult>>? _exitPlanModeHandler;
@@ -590,6 +591,11 @@ internal void RegisterMcpAuthHandler(Func<McpAuthContext, Task<McpAuthResult?>>?
590591
_mcpAuthHandler = handler;
591592
}
592593

594+
internal void RegisterMcpHeadersRefreshHandler(Func<McpHeadersRefreshContext, Task<McpHeadersRefreshResult?>>? handler)
595+
{
596+
_mcpHeadersRefreshHandler = handler;
597+
}
598+
593599
/// <summary>
594600
/// Handles a permission request from the Copilot CLI.
595601
/// </summary>
@@ -699,6 +705,26 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent)
699705
break;
700706
}
701707

708+
case McpHeadersRefreshRequiredEvent refreshEvent:
709+
{
710+
var data = refreshEvent.Data;
711+
var handler = _mcpHeadersRefreshHandler;
712+
if (string.IsNullOrEmpty(data.RequestId) || handler is null)
713+
return;
714+
715+
await ExecuteMcpHeadersRefreshAndRespondAsync(
716+
data.RequestId,
717+
new McpHeadersRefreshContext
718+
{
719+
SessionId = SessionId,
720+
ServerName = data.ServerName,
721+
ServerUrl = data.ServerUrl,
722+
Reason = data.Reason
723+
},
724+
handler);
725+
break;
726+
}
727+
702728
case CommandExecuteEvent cmdEvent:
703729
{
704730
var data = cmdEvent.Data;
@@ -826,6 +852,56 @@ private async Task ExecuteMcpAuthAndRespondAsync(
826852
}
827853
}
828854

855+
private async Task ExecuteMcpHeadersRefreshAndRespondAsync(
856+
string requestId,
857+
McpHeadersRefreshContext context,
858+
Func<McpHeadersRefreshContext, Task<McpHeadersRefreshResult?>> handler)
859+
{
860+
McpHeadersHandlePendingHeadersRefreshRequest response;
861+
try
862+
{
863+
var result = await handler(context);
864+
response = result is null
865+
? new McpHeadersHandlePendingHeadersRefreshRequestNone()
866+
: new McpHeadersHandlePendingHeadersRefreshRequestHeaders
867+
{
868+
Headers = result.Headers,
869+
TtlMs = result.TtlMs
870+
};
871+
}
872+
catch (OperationCanceledException ex)
873+
{
874+
response = new McpHeadersHandlePendingHeadersRefreshRequestError
875+
{
876+
Message = ex.Message
877+
};
878+
}
879+
catch (Exception ex) when (IsRecoverableMcpAuthFailure(ex))
880+
{
881+
response = new McpHeadersHandlePendingHeadersRefreshRequestError
882+
{
883+
Message = ex.Message
884+
};
885+
}
886+
887+
try
888+
{
889+
await Rpc.Mcp.Headers.HandlePendingHeadersRefreshRequestAsync(requestId, response);
890+
}
891+
catch (IOException)
892+
{
893+
// Connection lost — nothing we can do.
894+
}
895+
catch (ObjectDisposedException)
896+
{
897+
// Connection already disposed — nothing we can do.
898+
}
899+
catch (RemoteRpcException)
900+
{
901+
// The pending request may already be gone — nothing we can do.
902+
}
903+
}
904+
829905
private static bool IsRecoverableMcpAuthFailure(Exception exception)
830906
=> exception is not OperationCanceledException
831907
and not OutOfMemoryException

0 commit comments

Comments
 (0)