Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 6 additions & 4 deletions .github/workflows/sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,9 @@ jobs:
name: "Check schema and SDK freshness"
needs: detect-layout
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
timeout-minutes: 10
Expand All @@ -311,11 +314,10 @@ jobs:
with:
install-deps: "false"
- if: needs.detect-layout.outputs.runtime-source == 'checkout'
uses: ./.github/actions/setup-rust-cache
timeout-minutes: 8
continue-on-error: true
uses: ./.github/actions/setup-bazel
with:
cache-key: sdk-codegen
datadog-api-key: ${{ secrets.DATADOG_API_KEY }}
datadog-site: ${{ vars.DATADOG_SITE || vars.DD_SITE }}
- uses: actions/setup-go@v6
with:
go-version: "1.24"
Expand Down
115 changes: 115 additions & 0 deletions docs/features/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,124 @@ directories for different applications.
| `type` | `"http"` or `"sse"` | Yes | Server type |
| `url` | `string` | Yes | Server URL |
| `headers` | `object` | No | HTTP headers (e.g., for auth) |
| `oauthClientId` | `string` | No | Non-empty client ID for a statically configured OAuth client |
| `oauthScopes` | `string[]` | No | Non-empty array of valid RFC 6749 scope tokens. Requires `oauthClientId`; used when the server challenge omits scope or provides an empty scope, before protected-resource metadata fallback |
| `oauthPublicClient` | `boolean` | No | Whether the configured OAuth client is public and does not require a client secret |
| `oauthGrantType` | `string` | No | OAuth grant type for the configured client, such as `client_credentials` |
| `tools` | `string[]` | No | Tools to enable |
| `timeout` | `number` | No | Timeout in milliseconds |

## Session-scoped MCP diagnostics

Set `diagnostics: { sources: { mcp: { level: "debug" } } }` when you create or
resume a session to opt in to MCP diagnostics. Every source defaults to `off`.
The enabled levels are `error`, `warning`, `info`, `debug`, and `trace`. This
setting is separate from the client's process-level `logLevel`.

> [!WARNING]
> Diagnostic entries can contain user-provided or server-provided content at
> every enabled level: `warning` and above can include server stderr, while
> `debug` and `trace` can additionally include MCP payloads, tool arguments,
> and paths. Treat every entry as sensitive. Do not automatically upload entries
> as telemetry or export them without deliberate host action.

The generated `session.rpc.diagnostics` API lets hosts configure and read
session-scoped, bounded in-memory diagnostics. MCP is the first and currently
only supported source. Explicitly select it with `sources: ["mcp"]` when reading;
reading does not enable capture. Use one stoppable read loop
per output surface and retain its cursor. A read loop owns its cursor and
presentation; the runtime owns capture, filtering, retention, cursor expiry,
and wakeups.

```ts
import { approveAll, CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient();
const stop = new AbortController();
const appendLine = (line: string) => console.log(line);

await client.start();
const session = await client.createSession({
onPermissionRequest: approveAll,
diagnostics: { sources: { mcp: { level: "debug" } } },
});

try {
let cursor: string | undefined;
while (!stop.signal.aborted) {
const page = await session.rpc.diagnostics.read({
sources: ["mcp"],
cursor,
max: 100,
waitMs: 30_000,
});
if (page.cursorStatus === "expired") {
appendLine("MCP diagnostic entries were dropped.");
}
for (const entry of page.entries) {
const detail = entry.details.data ? ` ${entry.details.data}` : "";
appendLine(
`${entry.timestamp} [${entry.level}] [${entry.source}:${entry.details.serverName}] ${entry.message}${detail}`,
);
}
cursor = page.cursor;
}
} finally {
await session.disconnect();
await client.stop();
}
```

The abort signal stops the loop after its current bounded read completes; it is
not passed to the RPC call. Disabling capture wakes an outstanding read.

MCP startup is lazy: enabling or reading diagnostics does not start a server.
Configure `mcpServers` on session creation, then either use the session normally
or call `session.rpc.mcp.startServer({ serverName })` for an installed server
to troubleshoot initialization without making a model request.

Each record has `source: "mcp"`, timestamp, severity, message, and optional
`agentId` for non-root agents. Its typed `details` contains the lifecycle,
protocol, HTTP, or stderr category, server name, a fresh `connectionId` for each
connection attempt, and optional protocol direction and diagnostic data.
MCP retention is capped at 1,024 records and 4 MiB per session. Individual encoded
records are capped at 16 KiB and carry `truncated` when shortened. Reads default
to 100 records, accept a maximum of 500, and do not consume other readers' data.
Use `droppedCount`, when present on an expired cursor, to show the known loss.
Every successful read returns a cursor and cursor status, including empty
reads. Keep the returned cursor even when no records arrive. A cursor belongs
to its source selection; start without a cursor when changing that selection.
Missing, empty, duplicate, or unsupported sources are rejected, as are malformed
cursors and invalid numeric bounds. Neither records nor capture settings are
saved in session history.

Call `session.rpc.diagnostics.configure({ sources: { mcp: { level: "off" } } })`
to disable MCP capture and clear its retained buffer. Configuration updates only
the explicitly named sources and returns the effective source levels. Empty or
unknown source configuration is rejected; adding another supported source in
the future will not implicitly opt an existing caller into it.

Stop the host's read loop too; reads while logging
is off return immediately rather than waiting. An outstanding long poll wakes
when logging is disabled. Omitting `diagnostics` during a resident resume
preserves its current configuration; a cold-loaded session requires a new opt-in.
Enabling diagnostics from the initial `off` state can begin capture for an
already-live MCP connection. After diagnostics have been disabled with
`configure({ sources: { mcp: { level: "off" } } })`, re-enabling can resume capture for the same
live connection and its existing connection ID. Records that were already in
flight when diagnostics were disabled are discarded.

The runtime removes configured and structurally recognized credentials from HTTP
diagnostics before entries reach this API, but this is not a guarantee that
arbitrary server-provided secrets are absent. It does not expose request or
response header maps. Only selected metadata, such as `Content-Type`, `Accept`,
and `MCP-Protocol-Version`, is eligible for capture, but its values are still
untrusted and can contain sensitive content. Authentication headers, cookies,
session IDs, query values, URL userinfo, and fragments are omitted. URL paths
are retained for troubleshooting, so do not place credentials in path segments.
Protocol payloads and stderr can also contain user content or arbitrary
server-provided secrets.

## Troubleshooting

### Tools not showing up or not being invoked
Expand Down
52 changes: 36 additions & 16 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ Set `CopilotCliBinaryPath` to copy a preinstalled binary instead, or set

## Run the Samples

Try the interactive chat sample from the SDK root (`src/sdk` when nested).
For checkout builds, first follow [development setup](#development):
Try the interactive chat sample (from the repo root):

```bash
dotnet run --file dotnet/samples/Chat.cs
Expand Down Expand Up @@ -139,6 +138,7 @@ Create a new conversation session.
- `ReasoningEffort` - Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `ListModelsAsync()` to check which models support this option.
- `Tools` - Custom tool declarations exposed to the CLI. Declarations without an invocable `AIFunction` are left pending for manual resolution.
- `SystemMessage` - System message customization
- `RefreshCustomInstructions` - `true` invalidates process-wide custom-instruction discovery caches before constructing the new session. Omitted or `false` reuses the caches. Other sessions in this runtime may observe updated instructions on later turns or discovery. This does not watch files or override instruction enablement. Available on `SessionConfig`, not `ResumeSessionConfig`.
- `AvailableTools` - List of tool names to allow
- `ExcludedTools` - List of tool names to disable
- `Provider` - Custom API provider configuration (BYOK)
Expand Down Expand Up @@ -261,6 +261,10 @@ Use `MessageSource.System` for application-generated system context and
message's origin; it does not replace the session's system prompt or change
delivery mode. `SendAndWaitAsync` accepts the same option and still waits for
session idle, returning null if no assistant message was received.
Sub-agent events remain visible to listeners but do not complete the wait or
supply its reply.
Synchronous listeners registered before `SendAndWaitAsync` finish processing
the terminal event before the wait completes.

```csharp
await session.SendAsync(new MessageOptions
Expand Down Expand Up @@ -1239,6 +1243,16 @@ try
var session = await client.CreateSessionAsync();
await session.SendAsync(new MessageOptions { Prompt = "Hello" });
}
catch (IOException ex) when (ex.InnerException is RemoteRpcException)
{
var remote = (RemoteRpcException)ex.InnerException!;
Console.Error.WriteLine($"RPC error {remote.ErrorCode}: {remote.Message}");
if (remote.ErrorData is { } data)
{
// Interpret data according to the remote API's contract.
Console.Error.WriteLine($"Error data kind: {data.ValueKind}");
}
}
catch (IOException ex)
{
Console.Error.WriteLine($"Communication Error: {ex.Message}");
Expand All @@ -1249,28 +1263,34 @@ catch (Exception ex)
}
```

## Development
`RemoteRpcException` is in the `GitHub.Copilot` namespace. Remote JSON-RPC
errors remain wrapped in `IOException`; connection failures are not remote errors.
`ErrorData` is a `JsonElement?` that preserves objects, arrays, strings, numbers,
booleans, and empty values without converting them to application-specific types.
Omitted `data` has no nullable value; explicit JSON `null` has a value with
`ValueKind == JsonValueKind.Null`. The cloned element remains valid after the
response document or client is disposed. Exception messages and ordinary exception
formatting do not include the data payload.
Avoid logging it indiscriminately: server-provided data may contain sensitive
information.

Follow [SDK development setup](../CONTRIBUTING.md#developing-an-sdk) for the
.NET SDK selected by `global.json`, the **.NET 8 test runtime**, and Node/harness
dependencies. SDK 10 alone does not install the runtime for `net8.0` tests;
Windows additionally runs `net472` tests.
## Development

From the SDK root (`src/sdk` in the runtime repository, or the standalone
repository root):
Development requires [.NET SDK 10+](https://dotnet.microsoft.com/download) and a supported [Node.js version](../nodejs/README.md#prerequisites). From the repository root:

```bash
npm run build:dotnet
npm run test:dotnet
npm run check:dotnet
cd nodejs
npm ci
```

For a focused native test, first
[prepare the runtime](../CONTRIBUTING.md#testing-an-unreleased-runtime-api),
then run from `dotnet/` so `global.json` applies:
```bash
cd test/harness
npm ci
```

```bash
dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter "FullyQualifiedName~<test-name>"
cd dotnet
dotnet test
```

## License
Expand Down
6 changes: 6 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.Diagnostics,
config.McpOAuthTokenStorage,
config.AuthClientIdMetadataUrl,
"direct",
Expand Down Expand Up @@ -1284,6 +1285,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
GitHubTokenProviderRegistrationId: registrationId,
RemoteSession: config.RemoteSession,
Cloud: config.Cloud,
RefreshCustomInstructions: config.RefreshCustomInstructions,
InstructionDirectories: config.InstructionDirectories,
PluginDirectories: config.PluginDirectories,
DisabledMcpServers: config.DisabledMcpServers,
Expand Down Expand Up @@ -1506,6 +1508,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Streaming is true ? true : null,
config.IncludeSubAgentStreamingEvents,
config.McpServers,
config.Diagnostics,
config.McpOAuthTokenStorage,
config.AuthClientIdMetadataUrl,
"direct",
Expand Down Expand Up @@ -3073,6 +3076,7 @@ internal record CreateSessionRequest(
bool? Streaming,
bool? IncludeSubAgentStreamingEvents,
IDictionary<string, McpServerConfig>? McpServers,
DiagnosticsConfiguration? Diagnostics,
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
string? AuthClientIdMetadataUrl,
string? EnvValueMode,
Expand Down Expand Up @@ -3104,6 +3108,7 @@ internal record CreateSessionRequest(
[property: JsonPropertyName("gitHubTokenProviderRegistrationId")] string? GitHubTokenProviderRegistrationId = null,
RemoteSessionMode? RemoteSession = null,
CloudSessionOptions? Cloud = null,
bool? RefreshCustomInstructions = null,
IList<string>? InstructionDirectories = null,
IList<string>? PluginDirectories = null,
[property: JsonPropertyName("disabledMcpServers")] IList<string>? DisabledMcpServers = null,
Expand Down Expand Up @@ -3203,6 +3208,7 @@ internal record ResumeSessionRequest(
bool? Streaming,
bool? IncludeSubAgentStreamingEvents,
IDictionary<string, McpServerConfig>? McpServers,
DiagnosticsConfiguration? Diagnostics,
McpOAuthTokenStorageMode? McpOAuthTokenStorage,
string? AuthClientIdMetadataUrl,
string? EnvValueMode,
Expand Down
Loading
Loading