From 54aa2d60e4269bad9e4b7f5114b5080005a34ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Fri, 4 Sep 2026 17:07:21 +0300 Subject: [PATCH 1/2] Generate typed Java catalogue candidates --- java/scripts/codegen/java.test.ts | 118 ++++++++++++++ java/scripts/codegen/java.ts | 151 ++++++++++++++++-- java/scripts/codegen/package.json | 3 +- .../rpc/CatalogAiSkillCandidate.java | 93 +++++++++++ .../CatalogAiSkillCandidateProvenance.java | 31 ++++ .../generated/rpc/CatalogCandidate.java | 35 ++++ .../generated/rpc/CatalogCandidateSource.java | 35 ++++ .../rpc/CatalogCandidateSourceEmbedded.java | 30 ++++ .../rpc/CatalogCandidateSourceUrl.java | 37 +++++ .../rpc/CatalogMcpServerCandidate.java | 93 +++++++++++ .../CatalogMcpServerCandidateProvenance.java | 31 ++++ .../rpc/CatalogMcpServerInstallability.java | 35 ++++ .../generated/rpc/CatalogSearchSucceeded.java | 6 +- .../rpc/CatalogCandidateJacksonTest.java | 83 ++++++++++ 14 files changed, 762 insertions(+), 19 deletions(-) create mode 100644 java/scripts/codegen/java.test.ts create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java create mode 100644 java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java create mode 100644 java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java diff --git a/java/scripts/codegen/java.test.ts b/java/scripts/codegen/java.test.ts new file mode 100644 index 0000000000..8db96ee3d9 --- /dev/null +++ b/java/scripts/codegen/java.test.ts @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { JSONSchema7 } from "json-schema"; + +import { + collectNestedDiscriminatedUnionTypeNames, + schemaTypeToJava, +} from "./java.js"; + +test("nested discriminated array items use their named Java type", () => { + const definitions: Record = { + MessageResult: { + anyOf: [ + { $ref: "#/definitions/MessageDelivered" }, + { $ref: "#/definitions/MessageRejected" }, + ], + }, + MessageDelivered: { + type: "object", + additionalProperties: false, + properties: { + status: { const: "delivered" }, + actions: { + type: "array", + items: { $ref: "#/definitions/ActionChoice" }, + }, + }, + }, + MessageRejected: { + type: "object", + additionalProperties: false, + properties: { + status: { const: "rejected" }, + reason: { type: "string" }, + }, + }, + ActionChoice: { + anyOf: [ + { $ref: "#/definitions/PhoneAction" }, + { $ref: "#/definitions/EmailAction" }, + ], + }, + PhoneAction: { + type: "object", + title: "PhoneAction", + additionalProperties: false, + properties: { + kind: { const: "phone" }, + number: { type: "string" }, + sources: { + type: "object", + additionalProperties: { $ref: "#/definitions/ActionSource" }, + }, + }, + }, + EmailAction: { + type: "object", + title: "EmailAction", + additionalProperties: false, + properties: { + kind: { const: "email" }, + address: { type: "string" }, + sources: { + type: "object", + additionalProperties: { $ref: "#/definitions/ActionSource" }, + }, + }, + }, + ActionSource: { + anyOf: [ + { $ref: "#/definitions/LocalActionSource" }, + { $ref: "#/definitions/RemoteActionSource" }, + ], + }, + LocalActionSource: { + type: "object", + title: "LocalActionSource", + additionalProperties: false, + properties: { + location: { const: "local" }, + }, + }, + RemoteActionSource: { + type: "object", + title: "RemoteActionSource", + additionalProperties: false, + properties: { + location: { const: "remote" }, + url: { type: "string" }, + }, + }, + }; + const standaloneTypes = new Map(); + const promotedUnionTypes = collectNestedDiscriminatedUnionTypeNames( + { $ref: "#/definitions/MessageResult" }, + definitions + ); + + const result = schemaTypeToJava( + { + type: "array", + items: { $ref: "#/definitions/ActionChoice" }, + }, + false, + "MessageEnvelope", + "actions", + new Map(), + { + definitions, + standaloneTypes, + promotedUnionTypes, + } + ); + + assert.equal(result.javaType, "List"); + assert.deepEqual([...standaloneTypes.keys()], ["ActionChoice"]); + assert.deepEqual([...promotedUnionTypes], ["ActionChoice", "ActionSource"]); +}); diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 785049afa1..5bf945bb11 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -252,6 +252,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"), @@ -365,18 +366,101 @@ function findDiscriminator(variants: JSONSchema7[]): DiscriminatorInfo | null { /** * Resolve anyOf variants, handling $ref to definitions. */ -function resolveAnyOfVariants(anyOf: JSONSchema7[]): JSONSchema7[] { +function resolveAnyOfVariants( + anyOf: JSONSchema7[], + definitions: Record = currentDefinitions +): JSONSchema7[] { return anyOf .map((v) => { if (v.$ref) { const name = v.$ref.replace(/^#\/definitions\//, ""); - return currentDefinitions[name] ?? v; + return definitions[name] ?? v; } return v; }) .filter((v) => v.type !== "null"); } +export function collectNestedDiscriminatedUnionTypeNames( + root: unknown, + definitions: Record +): Set { + const promotedTypes = new Set(); + const definitionName = (schema: JSONSchema7): string | null => { + return schema.$ref?.match(/^#\/definitions\/([^/]+)$/)?.[1] ?? null; + }; + const resolveLocal = (schema: JSONSchema7): JSONSchema7 | null => { + const name = definitionName(schema); + return name ? definitions[name] ?? null : schema; + }; + const closedDiscriminatedUnionVariants = (schema: JSONSchema7): JSONSchema7[] | null => { + const resolved = resolveLocal(schema); + if (!resolved?.anyOf || !Array.isArray(resolved.anyOf)) return null; + const variants = resolveAnyOfVariants(resolved.anyOf as JSONSchema7[], definitions); + return variants.length > 1 + && findDiscriminator(variants) + && variants.every((variant) => variant.additionalProperties === false) + ? variants + : null; + }; + + const rootSchema = typeof root === "object" && root !== null ? root as JSONSchema7 : null; + const rootVariants = rootSchema ? closedDiscriminatedUnionVariants(rootSchema) : null; + if (!rootVariants) return promotedTypes; + + const nestedUnionItems: JSONSchema7[] = []; + for (const variant of rootVariants) { + for (const property of Object.values(variant.properties ?? {})) { + if (!property || typeof property !== "object") continue; + const propertySchema = resolveLocal(property as JSONSchema7); + if ( + propertySchema?.type === "array" + && propertySchema.items + && !Array.isArray(propertySchema.items) + && closedDiscriminatedUnionVariants(propertySchema.items as JSONSchema7) + ) { + nestedUnionItems.push(propertySchema.items as JSONSchema7); + } + } + } + + const visitedDefinitions = new Set(); + const visit = (schema: JSONSchema7): void => { + const name = definitionName(schema); + if (name) { + if (visitedDefinitions.has(name)) return; + visitedDefinitions.add(name); + const resolved = definitions[name]; + if (!resolved) return; + if (closedDiscriminatedUnionVariants(schema)) { + promotedTypes.add(name); + } + visit(resolved); + return; + } + + for (const property of Object.values(schema.properties ?? {})) { + if (property && typeof property === "object") { + visit(property as JSONSchema7); + } + } + if (schema.items && !Array.isArray(schema.items)) { + visit(schema.items as JSONSchema7); + } + if (schema.additionalProperties && typeof schema.additionalProperties === "object") { + visit(schema.additionalProperties as JSONSchema7); + } + for (const branch of [...(schema.anyOf ?? []), ...(schema.allOf ?? [])]) { + if (branch && typeof branch === "object") { + visit(branch as JSONSchema7); + } + } + }; + + for (const items of nestedUnionItems) visit(items); + return promotedTypes; +} + /** * Generate a polymorphic base class and variant subclasses for a discriminated union result type. */ @@ -578,12 +662,23 @@ async function generatePolymorphicVariantClass( await writeGeneratedFile(`${packageDir}/${className}.java`, lines.join("\n")); } -function schemaTypeToJava( +interface JavaTypeResolution { + definitions: Record; + standaloneTypes: Map; + promotedUnionTypes: Set; +} + +export function schemaTypeToJava( schema: JSONSchema7, required: boolean, context: string, propName: string, - nestedTypes: Map + nestedTypes: Map, + resolution: JavaTypeResolution = { + definitions: currentDefinitions, + standaloneTypes: pendingStandaloneTypes, + promotedUnionTypes: promotedNestedUnionTypes, + } ): JavaTypeResult { const imports = new Set(); @@ -606,16 +701,27 @@ function schemaTypeToJava( } const name = schema.$ref.replace(/^#\/definitions\//, ""); - const resolved = currentDefinitions[name]; + const resolved = resolution.definitions[name]; if (resolved) { + if ( + resolution.promotedUnionTypes.has(name) + && resolved.anyOf + && Array.isArray(resolved.anyOf) + ) { + const variants = resolveAnyOfVariants(resolved.anyOf as JSONSchema7[], resolution.definitions); + if (variants.length > 1 && findDiscriminator(variants)) { + resolution.standaloneTypes.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)) { - pendingStandaloneTypes.set(name, resolved); + resolution.standaloneTypes.set(name, resolved); return { javaType: name, imports }; } // Other types (primitives, arrays, maps, anyOf unions) → resolve and recurse - return schemaTypeToJava(resolved, required, context, propName, nestedTypes); + return schemaTypeToJava(resolved, required, context, propName, nestedTypes, resolution); } // Unresolved $ref — return name as-is console.warn(`[codegen] Unresolved $ref: ${schema.$ref}`); @@ -626,7 +732,8 @@ function schemaTypeToJava( 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"); if (nonNull.length === 1) { - const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, context, propName, nestedTypes); + const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, + context, propName, nestedTypes, resolution); return result; } // Multi-branch anyOf: fall through to Object, matching the C# generator's @@ -662,7 +769,8 @@ function schemaTypeToJava( const nonNullTypes = schema.type.filter((t) => t !== "null"); if (nonNullTypes.length === 1) { const baseSchema = { ...schema, type: nonNullTypes[0] }; - return schemaTypeToJava(baseSchema as JSONSchema7, required, context, propName, nestedTypes); + return schemaTypeToJava(baseSchema as JSONSchema7, required, context, propName, + nestedTypes, resolution); } } @@ -684,7 +792,8 @@ function schemaTypeToJava( const items = schema.items as JSONSchema7 | undefined; if (items) { // Always pass required=false so primitives are boxed (List, not List) - const itemResult = schemaTypeToJava(items, false, context, propName + "Item", nestedTypes); + const itemResult = schemaTypeToJava(items, false, context, propName + "Item", + nestedTypes, resolution); imports.add("java.util.List"); for (const imp of itemResult.imports) imports.add(imp); return { javaType: `List<${itemResult.javaType}>`, imports }; @@ -712,7 +821,8 @@ function schemaTypeToJava( ? schema.additionalProperties as JSONSchema7 : { type: "object" } as JSONSchema7; // Always pass required=false so primitives are boxed (Map, not Map) - const valueResult = schemaTypeToJava(valueSchema, false, context, propName + "Value", nestedTypes); + const valueResult = schemaTypeToJava(valueSchema, false, context, + propName + "Value", nestedTypes, resolution); imports.add("java.util.Map"); for (const imp of valueResult.imports) imports.add(imp); return { javaType: `Map`, imports }; @@ -1391,6 +1501,7 @@ async function generateRpcTypes(schemaPath: string): Promise { // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; pendingStandaloneTypes.clear(); + promotedNestedUnionTypes.clear(); crossSchemaDefinitions.clear(); // Load cross-schema definitions (session-events) so that cross-schema $ref values @@ -1415,6 +1526,14 @@ async function generateRpcTypes(schemaPath: string): Promise { if (schema.clientSession) sections.push(["clientSession", schema.clientSession]); if (schema.clientGlobal) sections.push(["clientGlobal", schema.clientGlobal]); + for (const [, sectionNode] of sections) { + for (const [, method] of collectRpcMethods(sectionNode)) { + for (const typeName of collectNestedDiscriminatedUnionTypeNames(method.result, currentDefinitions)) { + promotedNestedUnionTypes.add(typeName); + } + } + } + const generatedClasses = new Map(); const allFiles: string[] = []; @@ -2392,7 +2511,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.json b/java/scripts/codegen/package.json index 5ef4484fb7..054a48a607 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -4,7 +4,8 @@ "type": "module", "scripts": { "generate": "tsx java.ts", - "generate:java": "tsx java.ts" + "generate:java": "tsx java.ts", + "test": "tsx --test java.test.ts" }, "dependencies": { "@github/copilot": "^1.0.83-5", 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 candidates; + private List candidates; /** Whether further matches existed beyond the requested limit. */ @JsonProperty("truncated") @@ -48,8 +48,8 @@ public final class CatalogSearchSucceeded extends CatalogSearchResult { public String getSearchId() { return searchId; } public void setSearchId(String searchId) { this.searchId = searchId; } - public List getCandidates() { return candidates; } - public void setCandidates(List candidates) { this.candidates = candidates; } + public List getCandidates() { return candidates; } + public void setCandidates(List candidates) { this.candidates = candidates; } public Boolean getTruncated() { return truncated; } public void setTruncated(Boolean truncated) { this.truncated = truncated; } diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java new file mode 100644 index 0000000000..37be650118 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class CatalogCandidateJacksonTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void searchResult_deserializesTypedCandidatesAndSources() throws Exception { + var json = """ + { + "kind": "succeeded", + "searchId": "search-1", + "candidates": [ + { + "handle": "mcp-handle", + "handleExpiresAt": "2026-09-04T14:00:00Z", + "kind": "mcp-server", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP server", + "source": { + "kind": "url", + "url": "https://example.test/server.json" + }, + "provenance": { + "authority": "example.test", + "observedAt": "2026-09-04T13:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }, + { + "handle": "skill-handle", + "handleExpiresAt": "2026-09-04T14:00:00Z", + "kind": "ai-skill", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "source": { + "kind": "embedded" + }, + "provenance": { + "authority": "example.test", + "observedAt": "2026-09-04T13:00:00Z", + "mediaType": "application/ai-skill" + } + } + ], + "truncated": false, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] + } + } + """; + + var result = MAPPER.readValue(json, CatalogSearchResult.class); + var succeeded = assertInstanceOf(CatalogSearchSucceeded.class, result); + assertEquals(2, succeeded.getCandidates().size()); + + var mcp = assertInstanceOf(CatalogMcpServerCandidate.class, succeeded.getCandidates().get(0)); + var urlSource = assertInstanceOf(CatalogCandidateSourceUrl.class, mcp.getSource()); + assertEquals("https://example.test/server.json", urlSource.getUrl()); + + var skill = assertInstanceOf(CatalogAiSkillCandidate.class, succeeded.getCandidates().get(1)); + assertInstanceOf(CatalogCandidateSourceEmbedded.class, skill.getSource()); + + var serialized = MAPPER.valueToTree(result); + assertEquals("mcp-server", serialized.at("/candidates/0/kind").asText()); + assertEquals("url", serialized.at("/candidates/0/source/kind").asText()); + assertEquals("ai-skill", serialized.at("/candidates/1/kind").asText()); + assertEquals("embedded", serialized.at("/candidates/1/source/kind").asText()); + } +} From 0af57c44c78edc164ea6964f4c9b0459af1d0f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6khan=20Arkan?= Date: Fri, 4 Sep 2026 17:22:41 +0300 Subject: [PATCH 2/2] Preserve catalogue discriminator JSON --- java/scripts/codegen/java.ts | 5 ++++- .../copilot/generated/rpc/CatalogCandidate.java | 2 +- .../generated/rpc/CatalogCandidateSource.java | 2 +- .../generated/rpc/CatalogCandidateJacksonTest.java | 13 ++++++++----- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 5bf945bb11..9015ec62d2 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -513,7 +513,10 @@ async function generatePolymorphicResultClass( baseLines.push(` * @since 1.0.0`); baseLines.push(` */`); } - baseLines.push(`@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "${discriminator.property}", visible = true)`); + const typeInfoInclude = promotedNestedUnionTypes.has(className) + ? `, include = JsonTypeInfo.As.EXISTING_PROPERTY` + : ""; + baseLines.push(`@JsonTypeInfo(use = JsonTypeInfo.Id.NAME${typeInfoInclude}, property = "${discriminator.property}", visible = true)`); baseLines.push(`@JsonSubTypes({`); for (let i = 0; i < variantInfos.length; i++) { const v = variantInfos[i]; 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 index 7fc982cc21..a712236ddb 100644 --- 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 @@ -17,7 +17,7 @@ * * @since 1.0.0 */ -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = CatalogMcpServerCandidate.class, name = "mcp-server"), @JsonSubTypes.Type(value = CatalogAiSkillCandidate.class, name = "ai-skill") 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 index 90d9667d0d..798768739d 100644 --- 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 @@ -17,7 +17,7 @@ * * @since 1.0.0 */ -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind", visible = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = CatalogCandidateSourceUrl.class, name = "url"), @JsonSubTypes.Type(value = CatalogCandidateSourceEmbedded.class, name = "embedded") diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java index 37be650118..22af474624 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java @@ -74,10 +74,13 @@ void searchResult_deserializesTypedCandidatesAndSources() throws Exception { var skill = assertInstanceOf(CatalogAiSkillCandidate.class, succeeded.getCandidates().get(1)); assertInstanceOf(CatalogCandidateSourceEmbedded.class, skill.getSource()); - var serialized = MAPPER.valueToTree(result); - assertEquals("mcp-server", serialized.at("/candidates/0/kind").asText()); - assertEquals("url", serialized.at("/candidates/0/source/kind").asText()); - assertEquals("ai-skill", serialized.at("/candidates/1/kind").asText()); - assertEquals("embedded", serialized.at("/candidates/1/source/kind").asText()); + var serializedJson = MAPPER.writeValueAsString(result); + assertEquals(6, serializedJson.split("\"kind\"", -1).length - 1); + + var serializedTree = MAPPER.readTree(serializedJson); + assertEquals("mcp-server", serializedTree.at("/candidates/0/kind").asText()); + assertEquals("url", serializedTree.at("/candidates/0/source/kind").asText()); + assertEquals("ai-skill", serializedTree.at("/candidates/1/kind").asText()); + assertEquals("embedded", serializedTree.at("/candidates/1/source/kind").asText()); } }