diff --git a/.github/workflows/update-copilot-dependency.yml b/.github/workflows/update-copilot-dependency.yml
index 2870ad27f7..7374dab863 100644
--- a/.github/workflows/update-copilot-dependency.yml
+++ b/.github/workflows/update-copilot-dependency.yml
@@ -91,7 +91,7 @@ jobs:
env:
VERSION: ${{ inputs.version }}
working-directory: ./java/scripts/codegen
- run: npm install "@github/copilot@$VERSION"
+ run: npm install --save-exact "@github/copilot@$VERSION"
- name: Update Java POM CLI version property
env:
diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 2c74ab4bde..22fb609a95 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -2347,7 +2347,8 @@ internal sealed class DiscoveredExtensionsDisableRequest
[Experimental(Diagnostics.Experimental)]
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "kind",
- UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+ IgnoreUnrecognizedTypeDiscriminators = false,
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
[JsonDerivedType(typeof(CatalogSearchResultSucceeded), "succeeded")]
[JsonDerivedType(typeof(CatalogSearchResultNegotiationRefused), "negotiation-refused")]
[JsonDerivedType(typeof(CatalogSearchResultUnsupportedKind), "unsupported-kind")]
@@ -2362,6 +2363,7 @@ internal sealed class DiscoveredExtensionsDisableRequest
public partial class CatalogSearchResult
{
/// The type discriminator.
+ [JsonRequired]
[JsonPropertyName("kind")]
public virtual string Kind { get; set; } = string.Empty;
}
@@ -2372,12 +2374,14 @@ public partial class CatalogSearchResult
[Experimental(Diagnostics.Experimental)]
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "kind",
- UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+ IgnoreUnrecognizedTypeDiscriminators = false,
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
[JsonDerivedType(typeof(CatalogCandidateMcpServer), "mcp-server")]
[JsonDerivedType(typeof(CatalogCandidateAiSkill), "ai-skill")]
public partial class CatalogCandidate
{
/// The type discriminator.
+ [JsonRequired]
[JsonPropertyName("kind")]
public virtual string Kind { get; set; } = string.Empty;
}
@@ -2407,12 +2411,14 @@ public sealed class CatalogMcpServerCandidateProvenance
[Experimental(Diagnostics.Experimental)]
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "kind",
- UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
+ IgnoreUnrecognizedTypeDiscriminators = false,
+ UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
[JsonDerivedType(typeof(CatalogCandidateSourceUrl), "url")]
[JsonDerivedType(typeof(CatalogCandidateSourceEmbedded), "embedded")]
public partial class CatalogCandidateSource
{
/// The type discriminator.
+ [JsonRequired]
[JsonPropertyName("kind")]
public virtual string Kind { get; set; } = string.Empty;
}
diff --git a/dotnet/test/Unit/DiscriminatedUnionConformanceTests.cs b/dotnet/test/Unit/DiscriminatedUnionConformanceTests.cs
new file mode 100644
index 0000000000..d4332d9371
--- /dev/null
+++ b/dotnet/test/Unit/DiscriminatedUnionConformanceTests.cs
@@ -0,0 +1,173 @@
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+using GitHub.Copilot.Rpc;
+using Xunit;
+
+#pragma warning disable GHCP001 // The catalogue search schema is experimental.
+
+namespace GitHub.Copilot.Test.Unit;
+
+public class DiscriminatedUnionConformanceTests
+{
+ private const string OpaqueMcpHandle = "opaque:mcp/01-do-not-parse";
+ private const string OpaqueSkillHandle = "opaque:skill/02-do-not-parse";
+ private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
+ {
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
+ };
+
+ [Fact]
+ public void CatalogSearchResult_PreservesTypedCandidatesAndOpaqueHandles()
+ {
+ const string json = """
+ {
+ "kind": "succeeded",
+ "rawCard": { "secret": "must-not-survive" },
+ "searchId": "search-01",
+ "candidates": [
+ {
+ "kind": "mcp-server",
+ "handle": "opaque:mcp/01-do-not-parse",
+ "handleExpiresAt": "2026-09-02T12:00:00Z",
+ "mediaType": "application/mcp-server-card+json",
+ "installability": "installable",
+ "displayName": "Example MCP",
+ "rawCard": { "secret": "must-not-survive" },
+ "source": { "kind": "url", "url": "https://catalog.example/mcp.json", "rawCard": { "secret": "must-not-survive" } },
+ "provenance": {
+ "authority": "catalog.example",
+ "observedAt": "2026-09-02T11:00:00Z",
+ "mediaType": "application/mcp-server-card+json"
+ }
+ },
+ {
+ "kind": "ai-skill",
+ "handle": "opaque:skill/02-do-not-parse",
+ "handleExpiresAt": "2026-09-02T12:00:00Z",
+ "mediaType": "application/ai-skill",
+ "installability": "not-installable-kind",
+ "displayName": "Example skill",
+ "rawCard": { "secret": "must-not-survive" },
+ "source": { "kind": "embedded", "rawCard": { "secret": "must-not-survive" } },
+ "provenance": {
+ "authority": "catalog.example",
+ "observedAt": "2026-09-02T11:00:00Z",
+ "mediaType": "application/ai-skill"
+ }
+ }
+ ],
+ "truncated": false,
+ "negotiated": {
+ "runtimeProtocolVersion": 1,
+ "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"]
+ }
+ }
+ """;
+
+ var result = Assert.IsType(
+ JsonSerializer.Deserialize(json, SerializerOptions));
+ var mcp = Assert.IsType(result.Candidates[0]);
+ var skill = Assert.IsType(result.Candidates[1]);
+ Assert.Equal(OpaqueMcpHandle, mcp.Handle);
+ Assert.Equal(OpaqueSkillHandle, skill.Handle);
+ Assert.IsType(mcp.Source);
+ Assert.IsType(skill.Source);
+
+ using var encoded = JsonDocument.Parse(JsonSerializer.Serialize(
+ result, SerializerOptions));
+ Assert.False(encoded.RootElement.TryGetProperty("rawCard", out _));
+ foreach (var candidate in encoded.RootElement.GetProperty("candidates").EnumerateArray())
+ {
+ Assert.False(candidate.TryGetProperty("card", out _));
+ Assert.False(candidate.TryGetProperty("cardData", out _));
+ Assert.False(candidate.TryGetProperty("rawCard", out _));
+ Assert.False(candidate.GetProperty("source").TryGetProperty("rawCard", out _));
+ }
+ }
+
+ [Fact]
+ public void CatalogSearchResult_PreservesRefusalsAndFailures()
+ {
+ var authentication = JsonSerializer.Deserialize(
+ """{"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."}""",
+ SerializerOptions);
+ Assert.IsType(authentication);
+
+ var network = Assert.IsType(
+ JsonSerializer.Deserialize(
+ """{"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."}""",
+ SerializerOptions));
+ Assert.Equal(30, network.RetryAfterSeconds);
+ }
+
+ [Fact]
+ public void ClosedUnions_RejectUnknownAndMissingDiscriminators()
+ {
+ string[] invalidPayloads =
+ [
+ """{"kind":"future-result","rawCard":{"secret":"must-not-survive"}}""",
+ """{"rawCard":{"secret":"must-not-survive"}}""",
+ """
+ {
+ "kind":"succeeded",
+ "searchId":"search-invalid",
+ "candidates":[{"kind":"future-candidate","rawCard":{"secret":"must-not-survive"}}],
+ "truncated":false,
+ "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}
+ }
+ """,
+ """
+ {
+ "kind":"succeeded",
+ "searchId":"search-invalid",
+ "candidates":[{"rawCard":{"secret":"must-not-survive"}}],
+ "truncated":false,
+ "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}
+ }
+ """,
+ """
+ {
+ "kind":"succeeded",
+ "searchId":"search-invalid",
+ "candidates":[{
+ "kind":"mcp-server",
+ "handle":"opaque:mcp/01-do-not-parse",
+ "handleExpiresAt":"2026-09-02T12:00:00Z",
+ "mediaType":"application/mcp-server-card+json",
+ "installability":"installable",
+ "displayName":"Example MCP",
+ "source":{"kind":"future-source","rawCard":{"secret":"must-not-survive"}},
+ "provenance":{"authority":"catalog.example","observedAt":"2026-09-02T11:00:00Z","mediaType":"application/mcp-server-card+json"}
+ }],
+ "truncated":false,
+ "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}
+ }
+ """,
+ """
+ {
+ "kind":"succeeded",
+ "searchId":"search-invalid",
+ "candidates":[{
+ "kind":"mcp-server",
+ "handle":"opaque:mcp/01-do-not-parse",
+ "handleExpiresAt":"2026-09-02T12:00:00Z",
+ "mediaType":"application/mcp-server-card+json",
+ "installability":"installable",
+ "displayName":"Example MCP",
+ "source":{"rawCard":{"secret":"must-not-survive"}},
+ "provenance":{"authority":"catalog.example","observedAt":"2026-09-02T11:00:00Z","mediaType":"application/mcp-server-card+json"}
+ }],
+ "truncated":false,
+ "negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}
+ }
+ """,
+ ];
+
+ foreach (string json in invalidPayloads)
+ {
+ Exception? exception = Record.Exception(() =>
+ JsonSerializer.Deserialize(json, SerializerOptions));
+ Assert.True(exception is JsonException, $"Invalid closed union payload was accepted: {json}");
+ }
+ }
+}
diff --git a/go/rpc/discriminated_union_conformance_test.go b/go/rpc/discriminated_union_conformance_test.go
new file mode 100644
index 0000000000..d05ba81754
--- /dev/null
+++ b/go/rpc/discriminated_union_conformance_test.go
@@ -0,0 +1,181 @@
+package rpc
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+const (
+ opaqueMCPHandle = "opaque:mcp/01-do-not-parse"
+ opaqueSkillHandle = "opaque:skill/02-do-not-parse"
+)
+
+func TestClosedDiscriminatedUnionPreservesKnownNestedVariants(t *testing.T) {
+ result, err := unmarshalCatalogSearchResult([]byte(`{
+ "kind":"succeeded",
+ "rawCard":{"secret":"must-not-survive"},
+ "searchId":"search-01",
+ "candidates":[
+ {
+ "kind":"mcp-server",
+ "handle":"opaque:mcp/01-do-not-parse",
+ "handleExpiresAt":"2026-09-02T12:00:00Z",
+ "mediaType":"application/mcp-server-card+json",
+ "installability":"installable",
+ "displayName":"Example MCP",
+ "rawCard":{"secret":"must-not-survive"},
+ "source":{"kind":"url","url":"https://catalog.example/mcp.json","rawCard":{"secret":"must-not-survive"}},
+ "provenance":{
+ "authority":"catalog.example",
+ "observedAt":"2026-09-02T11:00:00Z",
+ "mediaType":"application/mcp-server-card+json"
+ }
+ },
+ {
+ "kind":"ai-skill",
+ "handle":"opaque:skill/02-do-not-parse",
+ "handleExpiresAt":"2026-09-02T12:00:00Z",
+ "mediaType":"application/ai-skill",
+ "installability":"not-installable-kind",
+ "displayName":"Example skill",
+ "rawCard":{"secret":"must-not-survive"},
+ "source":{"kind":"embedded","rawCard":{"secret":"must-not-survive"}},
+ "provenance":{
+ "authority":"catalog.example",
+ "observedAt":"2026-09-02T11:00:00Z",
+ "mediaType":"application/ai-skill"
+ }
+ }
+ ],
+ "truncated":false,
+ "negotiated":{
+ "runtimeProtocolVersion":1,
+ "grantedCapabilities":["mcp-server-card","ai-skill-discovery"]
+ }
+ }`))
+ if err != nil {
+ t.Fatalf("unmarshal catalogue success: %v", err)
+ }
+
+ success, ok := result.(*CatalogSearchSucceeded)
+ if !ok {
+ t.Fatalf("catalogue result = %T, want *CatalogSearchSucceeded", result)
+ }
+ mcp, ok := success.Candidates[0].(*CatalogMCPServerCandidate)
+ if !ok {
+ t.Fatalf("first candidate = %T, want *CatalogMCPServerCandidate", success.Candidates[0])
+ }
+ skill, ok := success.Candidates[1].(*CatalogAiSkillCandidate)
+ if !ok {
+ t.Fatalf("second candidate = %T, want *CatalogAiSkillCandidate", success.Candidates[1])
+ }
+ if mcp.Handle != opaqueMCPHandle || skill.Handle != opaqueSkillHandle {
+ t.Fatalf("opaque handles changed: %q, %q", mcp.Handle, skill.Handle)
+ }
+ if _, ok := mcp.Source.(*CatalogCandidateSourceURL); !ok {
+ t.Fatalf("MCP source = %T, want *CatalogCandidateSourceURL", mcp.Source)
+ }
+ if _, ok := skill.Source.(*CatalogCandidateSourceEmbedded); !ok {
+ t.Fatalf("skill source = %T, want *CatalogCandidateSourceEmbedded", skill.Source)
+ }
+
+ encoded, err := json.Marshal(success)
+ if err != nil {
+ t.Fatalf("marshal catalogue success: %v", err)
+ }
+ var wire map[string]any
+ if err := json.Unmarshal(encoded, &wire); err != nil {
+ t.Fatalf("decode catalogue wire result: %v", err)
+ }
+ if _, exists := wire["rawCard"]; exists {
+ t.Fatalf("result leaked rawCard: %s", encoded)
+ }
+ for _, candidate := range wire["candidates"].([]any) {
+ fields := candidate.(map[string]any)
+ for _, forbidden := range []string{"card", "cardData", "rawCard"} {
+ if _, exists := fields[forbidden]; exists {
+ t.Fatalf("candidate leaked %q: %s", forbidden, encoded)
+ }
+ }
+ if _, exists := fields["source"].(map[string]any)["rawCard"]; exists {
+ t.Fatalf("candidate source leaked rawCard: %s", encoded)
+ }
+ }
+}
+
+func TestClosedDiscriminatedUnionPreservesRefusalsAndFailures(t *testing.T) {
+ tests := []struct {
+ name string
+ payload string
+ assert func(*testing.T, CatalogSearchResult)
+ }{
+ {
+ name: "authentication required",
+ payload: `{"kind":"authentication-required","reason":"no-credential","message":"Sign in is required."}`,
+ assert: func(t *testing.T, result CatalogSearchResult) {
+ if _, ok := result.(*CatalogAuthenticationRequiredError); !ok {
+ t.Fatalf("result = %T, want *CatalogAuthenticationRequiredError", result)
+ }
+ },
+ },
+ {
+ name: "network failure",
+ payload: `{"kind":"network-failure","reason":"timeout","retryAfterSeconds":30,"message":"The catalogue timed out."}`,
+ assert: func(t *testing.T, result CatalogSearchResult) {
+ failure, ok := result.(*CatalogNetworkFailureError)
+ if !ok {
+ t.Fatalf("result = %T, want *CatalogNetworkFailureError", result)
+ }
+ if failure.RetryAfterSeconds == nil || *failure.RetryAfterSeconds != 30 {
+ t.Fatalf("retryAfterSeconds = %v, want 30", failure.RetryAfterSeconds)
+ }
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result, err := unmarshalCatalogSearchResult([]byte(test.payload))
+ if err != nil {
+ t.Fatalf("unmarshal catalogue result: %v", err)
+ }
+ test.assert(t, result)
+ })
+ }
+}
+
+func TestClosedDiscriminatedUnionRejectsUnknownAndMissingDiscriminators(t *testing.T) {
+ validCandidatePrefix := `{
+ "handle":"opaque:mcp/01-do-not-parse",
+ "handleExpiresAt":"2026-09-02T12:00:00Z",
+ "mediaType":"application/mcp-server-card+json",
+ "installability":"installable",
+ "displayName":"Example MCP",
+ "provenance":{
+ "authority":"catalog.example",
+ "observedAt":"2026-09-02T11:00:00Z",
+ "mediaType":"application/mcp-server-card+json"
+ },`
+ searchPrefix := `{"kind":"succeeded","searchId":"search-invalid","candidates":[`
+ searchSuffix := `],"truncated":false,"negotiated":{"runtimeProtocolVersion":1,"grantedCapabilities":[]}}`
+ tests := map[string]string{
+ "unknown outer discriminator": `{"kind":"future-result","rawCard":{"secret":"must-not-survive"}}`,
+ "missing outer discriminator": `{"rawCard":{"secret":"must-not-survive"}}`,
+ "unknown candidate discriminator": searchPrefix + validCandidatePrefix +
+ `"kind":"future-candidate","source":{"kind":"url","url":"https://catalog.example/mcp.json"},"rawCard":{"secret":"must-not-survive"}}` + searchSuffix,
+ "missing candidate discriminator": searchPrefix + validCandidatePrefix +
+ `"source":{"kind":"url","url":"https://catalog.example/mcp.json"},"rawCard":{"secret":"must-not-survive"}}` + searchSuffix,
+ "unknown nested discriminator": searchPrefix + validCandidatePrefix +
+ `"kind":"mcp-server","source":{"kind":"future-source","rawCard":{"secret":"must-not-survive"}}}` + searchSuffix,
+ "missing nested discriminator": searchPrefix + validCandidatePrefix +
+ `"kind":"mcp-server","source":{"rawCard":{"secret":"must-not-survive"}}}` + searchSuffix,
+ }
+
+ for name, payload := range tests {
+ t.Run(name, func(t *testing.T) {
+ if _, err := unmarshalCatalogSearchResult([]byte(payload)); err == nil {
+ t.Fatal("invalid closed union payload must be rejected")
+ }
+ })
+ }
+}
diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go
index 13d190be22..2d921c41dc 100644
--- a/go/rpc/zrpc_encoding.go
+++ b/go/rpc/zrpc_encoding.go
@@ -696,9 +696,8 @@ func unmarshalCatalogCandidate(data []byte) (CatalogCandidate, error) {
return nil, err
}
return &d, nil
- default:
- return &RawCatalogCandidateData{Discriminator: raw.Kind, Raw: data}, nil
}
+ return nil, errors.New("data did not match any union variant for CatalogCandidate")
}
func (r RawCatalogCandidateData) MarshalJSON() ([]byte, error) {
@@ -737,9 +736,8 @@ func unmarshalCatalogCandidateSource(data []byte) (CatalogCandidateSource, error
return nil, err
}
return &d, nil
- default:
- return &RawCatalogCandidateSourceData{Discriminator: raw.Kind, Raw: data}, nil
}
+ return nil, errors.New("data did not match any union variant for CatalogCandidateSource")
}
func (r RawCatalogCandidateSourceData) MarshalJSON() ([]byte, error) {
@@ -944,9 +942,8 @@ func unmarshalCatalogSearchResult(data []byte) (CatalogSearchResult, error) {
return nil, err
}
return &d, nil
- default:
- return &RawCatalogSearchResultData{Discriminator: raw.Kind, Raw: data}, nil
}
+ return nil, errors.New("data did not match any union variant for CatalogSearchResult")
}
func (r RawCatalogSearchResultData) MarshalJSON() ([]byte, error) {
diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts
index 785049afa1..71b191d956 100644
--- a/java/scripts/codegen/java.ts
+++ b/java/scripts/codegen/java.ts
@@ -11,6 +11,10 @@ import fs from "fs/promises";
import type { JSONSchema7 } from "json-schema";
import path from "path";
import { fileURLToPath } from "url";
+import {
+ analyseDiscriminatedUnionVariants,
+ analyseNestedClosedUnionResult,
+} from "../../../scripts/codegen/schema-unions.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -252,6 +256,7 @@ interface JavaTypeResult {
// Set before each schema generation pass; used by schemaTypeToJava and helpers.
let currentDefinitions: Record = {};
const pendingStandaloneTypes = new Map();
+const promotedNestedUnionTypes = new Set();
const generatedSessionEventTypeNames = new Set();
// Cross-schema definitions: keyed by schema filename (e.g. "session-events.schema.json"),
@@ -300,14 +305,14 @@ function resolveMethodParamsSchema(method: RpcMethodNode): JSONSchema7 | undefin
if (!params || typeof params !== "object") return undefined;
if (params.properties) return params;
if (!Array.isArray(params.anyOf)) return undefined;
- const objectVariants = resolveAnyOfVariants(params.anyOf as JSONSchema7[]).filter((variant) => !!variant.properties);
+ const objectVariants = resolveUnionVariants(params.anyOf as JSONSchema7[]).filter((variant) => !!variant.properties);
return hasOmissionSentinel(params) && objectVariants.length === 1 ? objectVariants[0] : undefined;
}
function resolveMethodParamsUnionSchema(method: RpcMethodNode): JSONSchema7 | undefined {
const params = resolveRef(method.params ?? undefined);
if (!params || typeof params !== "object" || !Array.isArray(params.anyOf)) return undefined;
- const variants = resolveAnyOfVariants(params.anyOf as JSONSchema7[]);
+ const variants = resolveUnionVariants(params.anyOf as JSONSchema7[]);
return variants.length > 1 && findDiscriminator(variants) ? params : undefined;
}
@@ -332,41 +337,31 @@ interface DiscriminatorInfo {
* A discriminator is a property with a `const` value that uniquely identifies each variant.
*/
function findDiscriminator(variants: JSONSchema7[]): DiscriminatorInfo | null {
- if (variants.length === 0) return null;
- const firstVariant = variants[0];
- if (!firstVariant.properties) return null;
-
- for (const [propName, propSchema] of Object.entries(firstVariant.properties).sort(([a], [b]) => a.localeCompare(b))) {
- if (typeof propSchema !== "object") continue;
- const schema = propSchema as JSONSchema7;
- if (schema.const === undefined) continue;
-
- const mapping = new Map();
- let isValidDiscriminator = true;
-
- for (const variant of variants) {
- if (!variant.properties) { isValidDiscriminator = false; break; }
- const variantProp = variant.properties[propName];
- if (typeof variantProp !== "object") { isValidDiscriminator = false; break; }
- const variantSchema = variantProp as JSONSchema7;
- if (variantSchema.const === undefined) { isValidDiscriminator = false; break; }
- const key = String(variantSchema.const);
- if (mapping.has(key)) { isValidDiscriminator = false; break; }
- mapping.set(key, { value: variantSchema.const, schema: variant });
- }
-
- if (isValidDiscriminator && mapping.size === variants.length) {
- return { property: propName, mapping };
- }
+ const analysis = analyseDiscriminatedUnionVariants(variants);
+ if (
+ !analysis ||
+ analysis.variants.some((variant) => variant.discriminatorValues.length !== 1) ||
+ analysis.mapping.some((entry) => entry.variants.length !== 1)
+ ) {
+ return null;
}
- return null;
+
+ return {
+ property: analysis.property,
+ mapping: new Map(
+ analysis.mapping.map((entry) => [
+ String(entry.value),
+ { value: entry.value, schema: entry.variants[0].schema },
+ ])
+ ),
+ };
}
/**
* Resolve anyOf variants, handling $ref to definitions.
*/
-function resolveAnyOfVariants(anyOf: JSONSchema7[]): JSONSchema7[] {
- return anyOf
+function resolveUnionVariants(variants: JSONSchema7[]): JSONSchema7[] {
+ return variants
.map((v) => {
if (v.$ref) {
const name = v.$ref.replace(/^#\/definitions\//, "");
@@ -377,6 +372,60 @@ function resolveAnyOfVariants(anyOf: JSONSchema7[]): JSONSchema7[] {
.filter((v) => v.type !== "null");
}
+export function collectNestedDiscriminatedUnionTypeNames(
+ root: unknown,
+ definitions: Record
+): Set {
+ const promotedTypes = new Set();
+ const visitedDefinitions = new Set();
+ const visit = (node: unknown, isRoot = false): void => {
+ if (Array.isArray(node)) {
+ for (const item of node) visit(item);
+ return;
+ }
+ if (!node || typeof node !== "object") return;
+
+ const schema = node as JSONSchema7;
+ if (schema.$ref?.startsWith("#/definitions/")) {
+ const name = schema.$ref.slice("#/definitions/".length);
+ if (visitedDefinitions.has(name)) return;
+ visitedDefinitions.add(name);
+ const definition = definitions[name];
+ if (definition) {
+ const union = definition.anyOf ?? definition.oneOf;
+ if (
+ union
+ && Array.isArray(union)
+ && analyseDiscriminatedUnionVariants(
+ union as JSONSchema7[],
+ (variant) => {
+ if (!variant.$ref?.startsWith("#/definitions/")) return variant;
+ return definitions[variant.$ref.slice("#/definitions/".length)];
+ }
+ )
+ && !isRoot
+ ) {
+ promotedTypes.add(name);
+ }
+ visit(definition);
+ }
+ return;
+ }
+
+ for (const value of Object.values(node as Record)) {
+ visit(value);
+ }
+ };
+ visit(root, true);
+ return promotedTypes;
+}
+
+function collectPromotedNestedUnionTypes(root: unknown): void {
+ for (const name of collectNestedDiscriminatedUnionTypeNames(root, currentDefinitions)) {
+ promotedNestedUnionTypes.add(name);
+ }
+}
+
/**
* Generate a polymorphic base class and variant subclasses for a discriminated union result type.
*/
@@ -386,8 +435,8 @@ async function generatePolymorphicResultClass(
packageName: string,
packageDir: string
): Promise {
- const anyOf = schema.anyOf as JSONSchema7[];
- const variants = resolveAnyOfVariants(anyOf);
+ const union = (schema.anyOf ?? schema.oneOf) as JSONSchema7[];
+ const variants = resolveUnionVariants(union);
const discriminator = findDiscriminator(variants);
if (!discriminator) {
@@ -408,7 +457,7 @@ async function generatePolymorphicResultClass(
variantInfos.push({ discriminatorValue: discValue, variantClassName, schema: variantSchema });
}
- // Generate the abstract base class
+ // Generate the polymorphic base class
const baseLines: string[] = [];
baseLines.push(COPYRIGHT);
baseLines.push("");
@@ -608,6 +657,18 @@ function schemaTypeToJava(
const name = schema.$ref.replace(/^#\/definitions\//, "");
const resolved = currentDefinitions[name];
if (resolved) {
+ const resolvedUnion = resolved.anyOf ?? resolved.oneOf;
+ if (
+ promotedNestedUnionTypes.has(name)
+ && resolvedUnion
+ && Array.isArray(resolvedUnion)
+ ) {
+ const variants = resolveUnionVariants(resolvedUnion as JSONSchema7[]);
+ if (variants.length > 1 && findDiscriminator(variants)) {
+ pendingStandaloneTypes.set(name, resolved);
+ return { javaType: name, imports };
+ }
+ }
// Enum or object types → register for standalone generation, return ref name
if ((resolved.type === "string" && resolved.enum) ||
(resolved.type === "object" && resolved.properties)) {
@@ -622,17 +683,25 @@ function schemaTypeToJava(
return { javaType: name, imports };
}
- if (schema.anyOf) {
- const hasNull = schema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null");
- const nonNull = schema.anyOf.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null");
+ const union = schema.anyOf ?? schema.oneOf;
+ if (union) {
+ const hasNull = union.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null");
+ const nonNull = union.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null");
if (nonNull.length === 1) {
const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, context, propName, nestedTypes);
return result;
}
- // Multi-branch anyOf: fall through to Object, matching the C# generator's
- // behavior. Java has no union types, so Object is the correct erasure for
- // anyOf[string, object] and similar multi-variant schemas.
- console.warn(`[codegen] ${context}.${propName}: anyOf with ${nonNull.length} non-null branches — falling back to Object`);
+ const variants = resolveUnionVariants(nonNull as JSONSchema7[]);
+ if (
+ variants.length > 1
+ && findDiscriminator(variants)
+ && schema.title
+ && promotedNestedUnionTypes.has(schema.title)
+ ) {
+ pendingStandaloneTypes.set(schema.title, schema);
+ return { javaType: schema.title, imports };
+ }
+ console.warn(`[codegen] ${context}.${propName}: union with ${nonNull.length} non-null branches — falling back to Object`);
return { javaType: "Object", imports };
}
@@ -1183,12 +1252,15 @@ async function generatePendingStandaloneTypes(
await generateStandaloneEnum(name, schema, packageName, packageDir, headerComment);
} else if (schema.type === "object" && schema.properties) {
await generateStandaloneRecord(name, schema, packageName, packageDir, headerComment);
- } else if (schema.anyOf && Array.isArray(schema.anyOf)) {
- const variants = resolveAnyOfVariants(schema.anyOf as JSONSchema7[]);
+ } else if (
+ (schema.anyOf && Array.isArray(schema.anyOf))
+ || (schema.oneOf && Array.isArray(schema.oneOf))
+ ) {
+ const variants = resolveUnionVariants((schema.anyOf ?? schema.oneOf) as JSONSchema7[]);
if (variants.length > 1 && findDiscriminator(variants)) {
await generatePolymorphicResultClass(name, schema, packageName, packageDir);
} else {
- console.warn(`[codegen] Cannot generate standalone type for ${name}: anyOf without discriminator`);
+ console.warn(`[codegen] Cannot generate standalone type for ${name}: union without discriminator`);
}
} else {
console.warn(`[codegen] Cannot generate standalone type for ${name}: type=${schema.type}`);
@@ -1391,7 +1463,16 @@ async function generateRpcTypes(schemaPath: string): Promise {
// Set module-level definitions for $ref resolution
currentDefinitions = (schema.definitions ?? {}) as Record;
pendingStandaloneTypes.clear();
+ promotedNestedUnionTypes.clear();
crossSchemaDefinitions.clear();
+ for (const section of [schema.server, schema.session, schema.clientSession, schema.clientGlobal]) {
+ if (!section) continue;
+ for (const [, method] of collectRpcMethods(section)) {
+ if (analyseNestedClosedUnionResult(method.result, currentDefinitions)) {
+ collectPromotedNestedUnionTypes(method.result);
+ }
+ }
+ }
// Load cross-schema definitions (session-events) so that cross-schema $ref values
// like "session-events.schema.json#/definitions/Foo" can be resolved.
@@ -1477,7 +1558,7 @@ async function generateRpcTypes(schemaPath: string): Promise {
pendingStandaloneTypes.set(resultRefName, resultSchema);
} else if (resultRefName && resultSchema.anyOf && Array.isArray(resultSchema.anyOf)) {
// anyOf discriminated union → generate polymorphic hierarchy
- const variants = resolveAnyOfVariants(resultSchema.anyOf as JSONSchema7[]);
+ const variants = resolveUnionVariants(resultSchema.anyOf as JSONSchema7[]);
if (variants.length > 1 && findDiscriminator(variants)) {
if (!generatedClasses.has(resultRefName)) {
generatedClasses.set(resultRefName, true);
@@ -1645,7 +1726,7 @@ function wrapperResultClassName(method: RpcMethodNode): string {
}
// anyOf discriminated union → use the definition name
if (resolved.anyOf && Array.isArray(resolved.anyOf)) {
- const variants = resolveAnyOfVariants(resolved.anyOf as JSONSchema7[]);
+ const variants = resolveUnionVariants(resolved.anyOf as JSONSchema7[]);
if (variants.length > 1 && findDiscriminator(variants)) {
return refName;
}
@@ -2392,7 +2473,9 @@ async function main(): Promise {
console.log("\n✅ Java code generation complete!");
}
-main().catch((err) => {
- console.error("❌ Code generation failed:", err);
- process.exit(1);
-});
+if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
+ main().catch((err) => {
+ console.error("❌ Code generation failed:", err);
+ process.exit(1);
+ });
+}
diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json
index 5e10cd839b..0d01b57ac0 100644
--- a/java/scripts/codegen/package-lock.json
+++ b/java/scripts/codegen/package-lock.json
@@ -6,7 +6,7 @@
"": {
"name": "copilot-sdk-java-codegen",
"dependencies": {
- "@github/copilot": "^1.0.83-5",
+ "@github/copilot": "1.0.83-5",
"json-schema": "^0.4.0",
"tsx": "^4.23.13"
}
diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json
index 5ef4484fb7..59e1aea0d2 100644
--- a/java/scripts/codegen/package.json
+++ b/java/scripts/codegen/package.json
@@ -7,7 +7,7 @@
"generate:java": "tsx java.ts"
},
"dependencies": {
- "@github/copilot": "^1.0.83-5",
+ "@github/copilot": "1.0.83-5",
"json-schema": "^0.4.0",
"tsx": "^4.23.13"
}
diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml
index aa40b25189..01374dec39 100644
--- a/java/sdk/pom.xml
+++ b/java/sdk/pom.xml
@@ -796,6 +796,7 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the
${project.parent.basedir}/scripts/codegen
install
+ --save-exact
@github/copilot@${copilot.schema.version}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java
new file mode 100644
index 0000000000..59e70f4935
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * An inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public final class CatalogAiSkillCandidate extends CatalogCandidate {
+
+ @JsonProperty("kind")
+ private final String kind = "ai-skill";
+
+ @Override
+ public String getKind() { return kind; }
+
+ /** Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. */
+ @JsonProperty("handle")
+ private String handle;
+
+ /** ISO 8601 timestamp after which the handle is stale and will be rejected. */
+ @JsonProperty("handleExpiresAt")
+ private String handleExpiresAt;
+
+ /** Media type of the underlying AI skill card */
+ @JsonProperty("mediaType")
+ private String mediaType;
+
+ /** AI skills are discovery-only and cannot be installed through this surface */
+ @JsonProperty("installability")
+ private String installability;
+
+ /** Display name taken verbatim from the card. Inert untrusted text. */
+ @JsonProperty("displayName")
+ private String displayName;
+
+ /** Description taken verbatim from the card. Inert untrusted text. */
+ @JsonProperty("description")
+ private String description;
+
+ /** Publisher taken verbatim from the card. Inert untrusted text. */
+ @JsonProperty("publisher")
+ private String publisher;
+
+ /** Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. */
+ @JsonProperty("source")
+ private CatalogCandidateSource source;
+
+ /** Where the catalog reference was observed, without the card itself or any content digest. */
+ @JsonProperty("provenance")
+ private CatalogAiSkillCandidateProvenance provenance;
+
+ public String getHandle() { return handle; }
+ public void setHandle(String handle) { this.handle = handle; }
+
+ public String getHandleExpiresAt() { return handleExpiresAt; }
+ public void setHandleExpiresAt(String handleExpiresAt) { this.handleExpiresAt = handleExpiresAt; }
+
+ public String getMediaType() { return mediaType; }
+ public void setMediaType(String mediaType) { this.mediaType = mediaType; }
+
+ public String getInstallability() { return installability; }
+ public void setInstallability(String installability) { this.installability = installability; }
+
+ public String getDisplayName() { return displayName; }
+ public void setDisplayName(String displayName) { this.displayName = displayName; }
+
+ public String getDescription() { return description; }
+ public void setDescription(String description) { this.description = description; }
+
+ public String getPublisher() { return publisher; }
+ public void setPublisher(String publisher) { this.publisher = publisher; }
+
+ public CatalogCandidateSource getSource() { return source; }
+ public void setSource(CatalogCandidateSource source) { this.source = source; }
+
+ public CatalogAiSkillCandidateProvenance getProvenance() { return provenance; }
+ public void setProvenance(CatalogAiSkillCandidateProvenance provenance) { this.provenance = provenance; }
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java
new file mode 100644
index 0000000000..0a2eff24e8
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java
@@ -0,0 +1,31 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind.
+ *
+ * @since 1.0.0
+ */
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CatalogAiSkillCandidateProvenance(
+ /** Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */
+ @JsonProperty("authority") String authority,
+ /** ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */
+ @JsonProperty("observedAt") String observedAt,
+ /** Media type advertised for the referenced AI skill card */
+ @JsonProperty("mediaType") String mediaType
+) {
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java
new file mode 100644
index 0000000000..7fc982cc21
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import javax.annotation.processing.Generated;
+
+/**
+ * One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other.
+ *
+ * @since 1.0.0
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = CatalogMcpServerCandidate.class, name = "mcp-server"),
+ @JsonSubTypes.Type(value = CatalogAiSkillCandidate.class, name = "ai-skill")
+})
+@JsonIgnoreProperties(ignoreUnknown = true)
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public abstract class CatalogCandidate {
+
+ /**
+ * Returns the discriminator value for this variant.
+ *
+ * @return the kind discriminator
+ */
+ public abstract String getKind();
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java
new file mode 100644
index 0000000000..90d9667d0d
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import javax.annotation.processing.Generated;
+
+/**
+ * Where a candidate's card came from. Exactly one of a URL or embedded data: the union has no variant carrying both, and no variant carrying neither, so the rule holds structurally rather than by validation.
+ *
+ * @since 1.0.0
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = CatalogCandidateSourceUrl.class, name = "url"),
+ @JsonSubTypes.Type(value = CatalogCandidateSourceEmbedded.class, name = "embedded")
+})
+@JsonIgnoreProperties(ignoreUnknown = true)
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public abstract class CatalogCandidateSource {
+
+ /**
+ * Returns the discriminator value for this variant.
+ *
+ * @return the kind discriminator
+ */
+ public abstract String getKind();
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java
new file mode 100644
index 0000000000..6814a0c4f2
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java
@@ -0,0 +1,30 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public final class CatalogCandidateSourceEmbedded extends CatalogCandidateSource {
+
+ @JsonProperty("kind")
+ private final String kind = "embedded";
+
+ @Override
+ public String getKind() { return kind; }
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java
new file mode 100644
index 0000000000..1c9b6d8905
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java
@@ -0,0 +1,37 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public final class CatalogCandidateSourceUrl extends CatalogCandidateSource {
+
+ @JsonProperty("kind")
+ private final String kind = "url";
+
+ @Override
+ public String getKind() { return kind; }
+
+ /** Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. */
+ @JsonProperty("url")
+ private String url;
+
+ public String getUrl() { return url; }
+ public void setUrl(String url) { this.url = url; }
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java
new file mode 100644
index 0000000000..8183ca422a
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * An inert MCP server catalog result. Every free-text field is untrusted external data and must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation.
+ *
+ * @since 1.0.0
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public final class CatalogMcpServerCandidate extends CatalogCandidate {
+
+ @JsonProperty("kind")
+ private final String kind = "mcp-server";
+
+ @Override
+ public String getKind() { return kind; }
+
+ /** Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. */
+ @JsonProperty("handle")
+ private String handle;
+
+ /** ISO 8601 timestamp after which the handle is stale and will be rejected. */
+ @JsonProperty("handleExpiresAt")
+ private String handleExpiresAt;
+
+ /** JSON MCP media type of the underlying card. */
+ @JsonProperty("mediaType")
+ private McpServerCardMediaType mediaType;
+
+ /** Whether this MCP server can be planned for installation, and if policy prevents it. */
+ @JsonProperty("installability")
+ private CatalogMcpServerInstallability installability;
+
+ /** Display name taken verbatim from the card. Inert untrusted text. */
+ @JsonProperty("displayName")
+ private String displayName;
+
+ /** Description taken verbatim from the card. Inert untrusted text. */
+ @JsonProperty("description")
+ private String description;
+
+ /** Publisher taken verbatim from the card. Inert untrusted text. */
+ @JsonProperty("publisher")
+ private String publisher;
+
+ /** Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. */
+ @JsonProperty("source")
+ private CatalogCandidateSource source;
+
+ /** Where the catalog reference was observed, without the card itself or any content digest. */
+ @JsonProperty("provenance")
+ private CatalogMcpServerCandidateProvenance provenance;
+
+ public String getHandle() { return handle; }
+ public void setHandle(String handle) { this.handle = handle; }
+
+ public String getHandleExpiresAt() { return handleExpiresAt; }
+ public void setHandleExpiresAt(String handleExpiresAt) { this.handleExpiresAt = handleExpiresAt; }
+
+ public McpServerCardMediaType getMediaType() { return mediaType; }
+ public void setMediaType(McpServerCardMediaType mediaType) { this.mediaType = mediaType; }
+
+ public CatalogMcpServerInstallability getInstallability() { return installability; }
+ public void setInstallability(CatalogMcpServerInstallability installability) { this.installability = installability; }
+
+ public String getDisplayName() { return displayName; }
+ public void setDisplayName(String displayName) { this.displayName = displayName; }
+
+ public String getDescription() { return description; }
+ public void setDescription(String description) { this.description = description; }
+
+ public String getPublisher() { return publisher; }
+ public void setPublisher(String publisher) { this.publisher = publisher; }
+
+ public CatalogCandidateSource getSource() { return source; }
+ public void setSource(CatalogCandidateSource source) { this.source = source; }
+
+ public CatalogMcpServerCandidateProvenance getProvenance() { return provenance; }
+ public void setProvenance(CatalogMcpServerCandidateProvenance provenance) { this.provenance = provenance; }
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java
new file mode 100644
index 0000000000..3ef13704ff
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java
@@ -0,0 +1,31 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * Where and when an MCP server catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind.
+ *
+ * @since 1.0.0
+ */
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CatalogMcpServerCandidateProvenance(
+ /** Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */
+ @JsonProperty("authority") String authority,
+ /** ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */
+ @JsonProperty("observedAt") String observedAt,
+ /** JSON MCP media type advertised for the referenced card. */
+ @JsonProperty("mediaType") McpServerCardMediaType mediaType
+) {
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java
new file mode 100644
index 0000000000..4478a81bd3
--- /dev/null
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java
@@ -0,0 +1,35 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.generated.rpc;
+
+import javax.annotation.processing.Generated;
+
+/**
+ * Whether an MCP server candidate can be planned for installation
+ *
+ * @since 1.0.0
+ */
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+public enum CatalogMcpServerInstallability {
+ /** The {@code installable} variant. */
+ INSTALLABLE("installable"),
+ /** The {@code not-installable-policy} variant. */
+ NOT_INSTALLABLE_POLICY("not-installable-policy");
+
+ private final String value;
+ CatalogMcpServerInstallability(String value) { this.value = value; }
+ @com.fasterxml.jackson.annotation.JsonValue
+ public String getValue() { return value; }
+ @com.fasterxml.jackson.annotation.JsonCreator
+ public static CatalogMcpServerInstallability fromValue(String value) {
+ for (CatalogMcpServerInstallability v : values()) {
+ if (v.value.equals(value)) return v;
+ }
+ throw new IllegalArgumentException("Unknown CatalogMcpServerInstallability value: " + value);
+ }
+}
diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java
index 8ecd11788a..b49f78faff 100644
--- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java
+++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java
@@ -35,7 +35,7 @@ public final class CatalogSearchSucceeded extends CatalogSearchResult {
/** Matching candidates, never more than the requested limit. All text is inert untrusted data. */
@JsonProperty("candidates")
- private List