From 2f81aadbe141cfed8d69ffdd6c4cb22037d5b7b6 Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Tue, 7 Jul 2026 06:17:03 -0500 Subject: [PATCH 1/3] Add optional implementation identity to the initialize handshake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce an `Implementation` type ({ name, version?, title? }) and add optional `InitializeResult.serverInfo` and `InitializeParams.clientInfo`, mirroring LSP's clientInfo/serverInfo and MCP's Implementation. The fields identify the host/client software and build behind either side of the handshake — informational only, explicitly not a feature-detection mechanism (that stays with the capability model). Regenerate all five client mirrors + JSON Schema, register the new type in each generator's exhaustiveness list, document the fields in the lifecycle spec, add a round-trip conformance fixture wired into all five client harnesses, and record CHANGELOG entries for the spec and every client. Closes #306 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 ++ clients/go/CHANGELOG.md | 5 ++ clients/go/ahptypes/commands.generated.go | 36 +++++++++++++++ clients/go/ahptypes/roundtrip_fixture_test.go | 4 ++ clients/kotlin/CHANGELOG.md | 4 ++ .../generated/Commands.generated.kt | 33 +++++++++++++ .../agenthostprotocol/RoundTripCorpusTest.kt | 2 + clients/rust/CHANGELOG.md | 5 ++ clients/rust/crates/ahp-types/src/commands.rs | 42 +++++++++++++++++ .../ahp-types/tests/roundtrip_corpus.rs | 3 +- .../Generated/Commands.generated.swift | 36 +++++++++++++++ .../TypesRoundTripFixtureTests.swift | 2 + clients/swift/CHANGELOG.md | 4 ++ clients/typescript/CHANGELOG.md | 4 ++ .../typescript/test/types-round-trip.test.ts | 2 + docs/specification/lifecycle.md | 12 +++++ schema/commands.schema.json | 29 ++++++++++++ schema/errors.schema.json | 29 ++++++++++++ scripts/generate-go.ts | 2 +- scripts/generate-kotlin.ts | 2 +- scripts/generate-rust.ts | 2 +- scripts/generate-swift.ts | 2 +- types/common/commands.ts | 46 +++++++++++++++++++ .../028-implementation-identity.json | 18 ++++++++ 24 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 types/test-cases/round-trips/028-implementation-identity.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd99323..b5b4c9cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,11 @@ Spec version: `0.5.2` - `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` and `disableUserInvocation` on `AgentCustomization`, giving custom agents and skills a symmetric user/model invocation matrix. +- Optional `serverInfo` on `InitializeResult` and `clientInfo` on + `InitializeParams`, each an `Implementation` (`name`, optional `version`, + optional `title`), so either side of the handshake can identify its + implementation and build. Informational only — mirrors LSP/MCP and MUST NOT + be used for feature detection. ### Changed diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index 75f985ac..2565512c 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -25,6 +25,11 @@ Implements AHP 0.5.2. `RuleCustomization`, `HookCustomization`). - `DisableUserInvocation` on `SkillCustomization`, plus `DisableModelInvocation` and `DisableUserInvocation` on `AgentCustomization`. +- Optional `ServerInfo` on `InitializeResult` and `ClientInfo` on + `InitializeParams`, each an `Implementation` struct (`Name`, optional + `Version`, optional `Title`), identifying the implementation and build behind + either side of the handshake. Informational only — MUST NOT be used for + feature detection. ### Changed diff --git a/clients/go/ahptypes/commands.generated.go b/clients/go/ahptypes/commands.generated.go index f8c29d8a..8388f390 100644 --- a/clients/go/ahptypes/commands.generated.go +++ b/clients/go/ahptypes/commands.generated.go @@ -94,6 +94,12 @@ type InitializeParams struct { ProtocolVersions []string `json:"protocolVersions"` // Unique client identifier ClientId string `json:"clientId"` + // Optional identity of the client implementation (name and version). + // Informational only — see {@link Implementation} for how it may and may not + // be used. Distinct from {@link InitializeParams.clientId | `clientId`}, + // which is an opaque per-connection identifier used for reconnection, not a + // human-readable implementation name. + ClientInfo *Implementation `json:"clientInfo,omitempty"` // URIs to subscribe to during handshake InitialSubscriptions []URI `json:"initialSubscriptions,omitempty"` // IETF BCP 47 language tag indicating the client's preferred locale @@ -122,6 +128,12 @@ type InitializeResult struct { ProtocolVersion string `json:"protocolVersion"` // Current server sequence number ServerSeq int64 `json:"serverSeq"` + // Optional identity of the server implementation (name and version). + // Informational only — see {@link Implementation} for how it may and may not + // be used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`} + // identifies the negotiated protocol, `serverInfo` identifies the host + // software behind it. + ServerInfo *Implementation `json:"serverInfo,omitempty"` // Snapshots for each `initialSubscriptions` URI Snapshots []Snapshot `json:"snapshots"` // Suggested default directory for remote filesystem browsing @@ -159,6 +171,30 @@ type ClientCapabilities struct { McpApps map[string]json.RawMessage `json:"mcpApps,omitempty"` } +// Identifies a protocol implementation — the software (and build) on one end +// of the connection, as distinct from the {@link AgentInfo | agent persona} it +// hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the +// client side and {@link InitializeResult.serverInfo | `serverInfo`} on the +// server side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's +// `Implementation`. +// +// This is **informational only**: it exists for logging, telemetry, an +// about/status affordance, and — as a last resort — a known-issue workaround +// for a specific buggy build. It is **not** a feature-detection mechanism. +// Feature availability stays with the capability model +// ({@link ClientCapabilities} and the various `*.capabilities` declarations); +// implementations SHOULD NOT gate protocol behaviour on parsing +// {@link Implementation.version | `version`}. +type Implementation struct { + // Implementation name, e.g. a product or package identifier. + Name string `json:"name"` + // Implementation version. A [SemVer](https://semver.org) string is + // recommended but not required. + Version *string `json:"version,omitempty"` + // Optional human-readable display name. + Title *string `json:"title,omitempty"` +} + // Re-establishes a dropped connection. The server replays missed actions or // provides fresh snapshots. type ReconnectParams struct { diff --git a/clients/go/ahptypes/roundtrip_fixture_test.go b/clients/go/ahptypes/roundtrip_fixture_test.go index 1cf7dd96..6810aa9b 100644 --- a/clients/go/ahptypes/roundtrip_fixture_test.go +++ b/clients/go/ahptypes/roundtrip_fixture_test.go @@ -234,6 +234,10 @@ func decodeAndReencode(t *testing.T, name, typ, inputJSON string) string { var v PartialSessionSummary dec(&v) return enc(&v) + case "Implementation": + var v Implementation + dec(&v) + return enc(&v) default: t.Fatalf("%s: round-trip fixture: unknown wire type %q. Add a decode entry to decodeAndReencode.", name, typ) return "" diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index c58f26c6..6e5269c4 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -26,6 +26,10 @@ Implements AHP 0.5.2. `RuleCustomization`, `HookCustomization`). - `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` and `disableUserInvocation` on `AgentCustomization`. +- Optional `serverInfo` on `InitializeResult` and `clientInfo` on + `InitializeParams`, each an `Implementation` (`name`, optional `version`, + optional `title`), identifying the implementation and build behind either side + of the handshake. Informational only — MUST NOT be used for feature detection. ### Changed diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt index 1dc2c60b..5f0c5318 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Commands.generated.kt @@ -123,6 +123,14 @@ data class InitializeParams( * Unique client identifier */ val clientId: String, + /** + * Optional identity of the client implementation (name and version). + * Informational only — see {@link Implementation} for how it may and may not + * be used. Distinct from {@link InitializeParams.clientId | `clientId`}, + * which is an opaque per-connection identifier used for reconnection, not a + * human-readable implementation name. + */ + val clientInfo: Implementation? = null, /** * URIs to subscribe to during handshake */ @@ -155,6 +163,14 @@ data class InitializeResult( * Current server sequence number */ val serverSeq: Long, + /** + * Optional identity of the server implementation (name and version). + * Informational only — see {@link Implementation} for how it may and may not + * be used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`} + * identifies the negotiated protocol, `serverInfo` identifies the host + * software behind it. + */ + val serverInfo: Implementation? = null, /** * Snapshots for each `initialSubscriptions` URI */ @@ -198,6 +214,23 @@ data class ClientCapabilities( val mcpApps: Map? = null ) +@Serializable +data class Implementation( + /** + * Implementation name, e.g. a product or package identifier. + */ + val name: String, + /** + * Implementation version. A [SemVer](https://semver.org) string is + * recommended but not required. + */ + val version: String? = null, + /** + * Optional human-readable display name. + */ + val title: String? = null +) + @Serializable data class ReconnectParams( /** diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt index 380b12d8..34de8a83 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/RoundTripCorpusTest.kt @@ -31,6 +31,7 @@ package com.microsoft.agenthostprotocol import com.microsoft.agenthostprotocol.generated.ActionEnvelope import com.microsoft.agenthostprotocol.generated.ChangesetOperationTarget import com.microsoft.agenthostprotocol.generated.Customization +import com.microsoft.agenthostprotocol.generated.Implementation import com.microsoft.agenthostprotocol.generated.JsonRpcErrorResponse import com.microsoft.agenthostprotocol.generated.JsonRpcNotification import com.microsoft.agenthostprotocol.generated.JsonRpcRequest @@ -247,6 +248,7 @@ class RoundTripCorpusTest { "SessionSummary" -> rt(SessionSummary.serializer()) "SessionAddedParams" -> rt(SessionAddedParams.serializer()) "PartialSessionSummary" -> rt(PartialSessionSummary.serializer()) + "Implementation" -> rt(Implementation.serializer()) else -> fail( "$file: unknown wire type \"$typeName\". " + "Add a decode entry to decodeAndReencode.", diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 2dc2620e..1638224f 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -27,6 +27,11 @@ Implements AHP 0.5.2. - `disable_user_invocation` on `SkillCustomization`, plus `disable_model_invocation` and `disable_user_invocation` on `AgentCustomization`. +- Optional `server_info` on `InitializeResult` and `client_info` on + `InitializeParams`, each an `Implementation` struct (`name`, optional + `version`, optional `title`), identifying the implementation and build behind + either side of the handshake. Informational only — MUST NOT be used for + feature detection. ### Changed diff --git a/clients/rust/crates/ahp-types/src/commands.rs b/clients/rust/crates/ahp-types/src/commands.rs index b84a6c08..2aca4605 100644 --- a/clients/rust/crates/ahp-types/src/commands.rs +++ b/clients/rust/crates/ahp-types/src/commands.rs @@ -109,6 +109,13 @@ pub struct InitializeParams { pub protocol_versions: Vec, /// Unique client identifier pub client_id: String, + /// Optional identity of the client implementation (name and version). + /// Informational only — see {@link Implementation} for how it may and may not + /// be used. Distinct from {@link InitializeParams.clientId | `clientId`}, + /// which is an opaque per-connection identifier used for reconnection, not a + /// human-readable implementation name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_info: Option, /// URIs to subscribe to during handshake #[serde(default, skip_serializing_if = "Option::is_none")] pub initial_subscriptions: Option>, @@ -142,6 +149,13 @@ pub struct InitializeResult { pub protocol_version: String, /// Current server sequence number pub server_seq: i64, + /// Optional identity of the server implementation (name and version). + /// Informational only — see {@link Implementation} for how it may and may not + /// be used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`} + /// identifies the negotiated protocol, `serverInfo` identifies the host + /// software behind it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub server_info: Option, /// Snapshots for each `initialSubscriptions` URI pub snapshots: Vec, /// Suggested default directory for remote filesystem browsing @@ -185,6 +199,34 @@ pub struct ClientCapabilities { pub mcp_apps: Option, } +/// Identifies a protocol implementation — the software (and build) on one end +/// of the connection, as distinct from the {@link AgentInfo | agent persona} it +/// hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the +/// client side and {@link InitializeResult.serverInfo | `serverInfo`} on the +/// server side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's +/// `Implementation`. +/// +/// This is **informational only**: it exists for logging, telemetry, an +/// about/status affordance, and — as a last resort — a known-issue workaround +/// for a specific buggy build. It is **not** a feature-detection mechanism. +/// Feature availability stays with the capability model +/// ({@link ClientCapabilities} and the various `*.capabilities` declarations); +/// implementations SHOULD NOT gate protocol behaviour on parsing +/// {@link Implementation.version | `version`}. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Implementation { + /// Implementation name, e.g. a product or package identifier. + pub name: String, + /// Implementation version. A [SemVer](https://semver.org) string is + /// recommended but not required. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option, + /// Optional human-readable display name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, +} + /// Re-establishes a dropped connection. The server replays missed actions or /// provides fresh snapshots. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs index f3d16c3b..25326fc8 100644 --- a/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs +++ b/clients/rust/crates/ahp-types/tests/roundtrip_corpus.rs @@ -26,7 +26,7 @@ use ahp_types::{ actions::{ActionEnvelope, StateAction}, - commands::ChangesetOperationTarget, + commands::{ChangesetOperationTarget, Implementation}, common::StringOrMarkdown, messages::JsonRpcMessage, notifications::{PartialSessionSummary, SessionAddedParams}, @@ -219,6 +219,7 @@ fn decode_and_reencode(file: &str, type_name: &str, input_json: &str) -> Result< "SessionSummary" => round_trip!(SessionSummary), "SessionAddedParams" => round_trip!(SessionAddedParams), "PartialSessionSummary" => round_trip!(PartialSessionSummary), + "Implementation" => round_trip!(Implementation), other => Err(format!( "{}: unknown wire type {:?}. Add a decode entry to decode_and_reencode.", file, other diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift index 6cb15a84..c78a8bfb 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Commands.generated.swift @@ -71,6 +71,12 @@ public struct InitializeParams: Codable, Sendable { public var protocolVersions: [String] /// Unique client identifier public var clientId: String + /// Optional identity of the client implementation (name and version). + /// Informational only — see {@link Implementation} for how it may and may not + /// be used. Distinct from {@link InitializeParams.clientId | `clientId`}, + /// which is an opaque per-connection identifier used for reconnection, not a + /// human-readable implementation name. + public var clientInfo: Implementation? /// URIs to subscribe to during handshake public var initialSubscriptions: [String]? /// IETF BCP 47 language tag indicating the client's preferred locale @@ -88,6 +94,7 @@ public struct InitializeParams: Codable, Sendable { channel: String, protocolVersions: [String], clientId: String, + clientInfo: Implementation? = nil, initialSubscriptions: [String]? = nil, locale: String? = nil, capabilities: ClientCapabilities? = nil @@ -95,6 +102,7 @@ public struct InitializeParams: Codable, Sendable { self.channel = channel self.protocolVersions = protocolVersions self.clientId = clientId + self.clientInfo = clientInfo self.initialSubscriptions = initialSubscriptions self.locale = locale self.capabilities = capabilities @@ -108,6 +116,12 @@ public struct InitializeResult: Codable, Sendable { public var protocolVersion: String /// Current server sequence number public var serverSeq: Int + /// Optional identity of the server implementation (name and version). + /// Informational only — see {@link Implementation} for how it may and may not + /// be used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`} + /// identifies the negotiated protocol, `serverInfo` identifies the host + /// software behind it. + public var serverInfo: Implementation? /// Snapshots for each `initialSubscriptions` URI public var snapshots: [Snapshot] /// Suggested default directory for remote filesystem browsing @@ -127,6 +141,7 @@ public struct InitializeResult: Codable, Sendable { public init( protocolVersion: String, serverSeq: Int, + serverInfo: Implementation? = nil, snapshots: [Snapshot], defaultDirectory: String? = nil, completionTriggerCharacters: [String]? = nil, @@ -134,6 +149,7 @@ public struct InitializeResult: Codable, Sendable { ) { self.protocolVersion = protocolVersion self.serverSeq = serverSeq + self.serverInfo = serverInfo self.snapshots = snapshots self.defaultDirectory = defaultDirectory self.completionTriggerCharacters = completionTriggerCharacters @@ -162,6 +178,26 @@ public struct ClientCapabilities: Codable, Sendable { } } +public struct Implementation: Codable, Sendable { + /// Implementation name, e.g. a product or package identifier. + public var name: String + /// Implementation version. A [SemVer](https://semver.org) string is + /// recommended but not required. + public var version: String? + /// Optional human-readable display name. + public var title: String? + + public init( + name: String, + version: String? = nil, + title: String? = nil + ) { + self.name = name + self.version = version + self.title = title + } +} + public struct ReconnectParams: Codable, Sendable { /// Channel URI this command targets. public var channel: String diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift index 62121b78..eb9f96cc 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolClientTests/TypesRoundTripFixtureTests.swift @@ -184,6 +184,8 @@ final class TypesRoundTripFixtureTests: XCTestCase { return try reencode(dec.decode(SessionAddedParams.self, from: inputData)) case "PartialSessionSummary": return try reencode(dec.decode(PartialSessionSummary.self, from: inputData)) + case "Implementation": + return try reencode(dec.decode(Implementation.self, from: inputData)) default: throw FixtureError.message( "round-trip fixture: unknown wire type \"\(type)\". Add a decode entry to decodeAndReencode.") diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index 78aa42b2..f2cfbc82 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -28,6 +28,10 @@ Implements AHP 0.5.2. `RuleCustomization`, `HookCustomization`). - `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` and `disableUserInvocation` on `AgentCustomization`. +- Optional `serverInfo` on `InitializeResult` and `clientInfo` on + `InitializeParams`, each an `Implementation` (`name`, optional `version`, + optional `title`), identifying the implementation and build behind either side + of the handshake. Informational only — MUST NOT be used for feature detection. ### Changed diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index f1774f66..ed0ac841 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -31,6 +31,10 @@ Implements AHP 0.5.2. `RuleCustomization`, `HookCustomization`). - `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` and `disableUserInvocation` on `AgentCustomization`. +- Optional `serverInfo` on `InitializeResult` and `clientInfo` on + `InitializeParams`, each an `Implementation` (`name`, optional `version`, + optional `title`), identifying the implementation and build behind either side + of the handshake. Informational only — MUST NOT be used for feature detection. ### Changed diff --git a/clients/typescript/test/types-round-trip.test.ts b/clients/typescript/test/types-round-trip.test.ts index 6e283b45..28c05753 100644 --- a/clients/typescript/test/types-round-trip.test.ts +++ b/clients/typescript/test/types-round-trip.test.ts @@ -59,6 +59,7 @@ import type { SessionSummary, } from '../src/types/channels-session/state.js'; import type { SessionAddedParams } from '../src/types/channels-root/notifications.js'; +import type { Implementation } from '../src/types/common/commands.js'; // ─── Fixture directory ─────────────────────────────────────────────────────── @@ -239,6 +240,7 @@ function bindToType(file: string, type: string, parsed: unknown): void { case 'SessionSummary': void (parsed as SessionSummary); break; case 'SessionAddedParams': void (parsed as SessionAddedParams); break; case 'PartialSessionSummary': void (parsed as Partial); break; + case 'Implementation': void (parsed as Implementation); break; default: throw new Error( `${file}: unknown wire type "${type}". Add a decode entry to bindToType.`, diff --git a/docs/specification/lifecycle.md b/docs/specification/lifecycle.md index 6bba6f23..5fc90338 100644 --- a/docs/specification/lifecycle.md +++ b/docs/specification/lifecycle.md @@ -24,6 +24,7 @@ The client initiates the connection with an `initialize` **request**. The client "channel": "ahp-root://", "protocolVersions": ["0.3.0"], "clientId": "client-abc", + "clientInfo": { "name": "acme-ide", "version": "2.5.0" }, "initialSubscriptions": ["ahp-root://"], "locale": "en-US" } @@ -36,6 +37,8 @@ The client initiates the connection with an `initialize` **request**. The client `locale` is an optional IETF BCP 47 language tag (e.g. `"en-US"`, `"ja"`) indicating the client's preferred language. The server SHOULD use this to localise user-facing strings such as confirmation option labels. +`clientInfo` optionally identifies the client *implementation* — its `name` and, optionally, `version` and display `title`. It is distinct from `clientId`, which is an opaque per-connection identifier used for reconnection. See [Implementation identity](#implementation-identity) below. + ### Initialize Response (Server → Client) ```json @@ -45,6 +48,7 @@ The client initiates the connection with an `initialize` **request**. The client "result": { "protocolVersion": "0.3.0", "serverSeq": 42, + "serverInfo": { "name": "acme-agent-host", "version": "1.4.2" }, "defaultDirectory": "file:///home/testuser", "snapshots": [ { @@ -63,6 +67,14 @@ If present, `defaultDirectory` provides a server-local starting location for rem If the server cannot accept the connection for any other reason, it MUST return a JSON-RPC error. See [Error Codes](/reference/error-codes) for defined codes. +### Implementation identity + +Both sides of the handshake MAY advertise the *implementation* behind them: the client via `InitializeParams.clientInfo` and the server via `InitializeResult.serverInfo`. Each is an `Implementation` (see the [`initialize`](/reference/common#initialize) reference) carrying a required `name` plus optional `version` and display `title`. This mirrors LSP's `clientInfo`/`serverInfo` and MCP's `Implementation`. + +Implementation identity is **informational only** — for logging, telemetry, an about/status affordance, and, as a last resort, a known-issue workaround for a specific buggy build. It answers "what software, and which build, is on the other end," which is distinct from both the negotiated `protocolVersion` and the [`AgentInfo`](/reference/root#agentinfo) that names the agent persona. + +It is **not** a feature-detection mechanism. Feature availability stays with the capability model (`ClientCapabilities` and the various `*.capabilities` declarations); clients and servers SHOULD NOT gate protocol behaviour on parsing `version`. Both fields are optional and purely additive: a peer that omits its own info, or ignores the other side's, stays fully interoperable. + ## Authentication Agents MAY declare `protectedResources` in their [`AgentInfo`](/reference/root#agentinfo). Before interacting with a session backed by such an agent, the client SHOULD authenticate by obtaining a Bearer token from the declared authorization server(s) and pushing it via the [`authenticate`](/reference/common#authenticate) command. diff --git a/schema/commands.schema.json b/schema/commands.schema.json index f260b622..f43bd481 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -42,6 +42,27 @@ } } }, + "Implementation": { + "type": "object", + "description": "Identifies a protocol implementation — the software (and build) on one end\nof the connection, as distinct from the {@link AgentInfo | agent persona} it\nhosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the\nclient side and {@link InitializeResult.serverInfo | `serverInfo`} on the\nserver side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's\n`Implementation`.\n\nThis is **informational only**: it exists for logging, telemetry, an\nabout/status affordance, and — as a last resort — a known-issue workaround\nfor a specific buggy build. It is **not** a feature-detection mechanism.\nFeature availability stays with the capability model\n({@link ClientCapabilities} and the various `*.capabilities` declarations);\nimplementations SHOULD NOT gate protocol behaviour on parsing\n{@link Implementation.version | `version`}.", + "properties": { + "name": { + "type": "string", + "description": "Implementation name, e.g. a product or package identifier." + }, + "version": { + "type": "string", + "description": "Implementation version. A [SemVer](https://semver.org) string is\nrecommended but not required." + }, + "title": { + "type": "string", + "description": "Optional human-readable display name." + } + }, + "required": [ + "name" + ] + }, "InitializeParams": { "type": "object", "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", @@ -63,6 +84,10 @@ "type": "string", "description": "Unique client identifier" }, + "clientInfo": { + "$ref": "#/$defs/Implementation", + "description": "Optional identity of the client implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Distinct from {@link InitializeParams.clientId | `clientId`},\nwhich is an opaque per-connection identifier used for reconnection, not a\nhuman-readable implementation name." + }, "initialSubscriptions": { "type": "array", "items": { @@ -108,6 +133,10 @@ "type": "number", "description": "Current server sequence number" }, + "serverInfo": { + "$ref": "#/$defs/Implementation", + "description": "Optional identity of the server implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`}\nidentifies the negotiated protocol, `serverInfo` identifies the host\nsoftware behind it." + }, "snapshots": { "type": "array", "items": { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 53234985..c5d9741b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -4539,6 +4539,27 @@ } } }, + "Implementation": { + "type": "object", + "description": "Identifies a protocol implementation — the software (and build) on one end\nof the connection, as distinct from the {@link AgentInfo | agent persona} it\nhosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the\nclient side and {@link InitializeResult.serverInfo | `serverInfo`} on the\nserver side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's\n`Implementation`.\n\nThis is **informational only**: it exists for logging, telemetry, an\nabout/status affordance, and — as a last resort — a known-issue workaround\nfor a specific buggy build. It is **not** a feature-detection mechanism.\nFeature availability stays with the capability model\n({@link ClientCapabilities} and the various `*.capabilities` declarations);\nimplementations SHOULD NOT gate protocol behaviour on parsing\n{@link Implementation.version | `version`}.", + "properties": { + "name": { + "type": "string", + "description": "Implementation name, e.g. a product or package identifier." + }, + "version": { + "type": "string", + "description": "Implementation version. A [SemVer](https://semver.org) string is\nrecommended but not required." + }, + "title": { + "type": "string", + "description": "Optional human-readable display name." + } + }, + "required": [ + "name" + ] + }, "InitializeParams": { "type": "object", "description": "Establishes a new connection and negotiates the protocol version.\nThis MUST be the first message sent by the client.", @@ -4560,6 +4581,10 @@ "type": "string", "description": "Unique client identifier" }, + "clientInfo": { + "$ref": "#/$defs/Implementation", + "description": "Optional identity of the client implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Distinct from {@link InitializeParams.clientId | `clientId`},\nwhich is an opaque per-connection identifier used for reconnection, not a\nhuman-readable implementation name." + }, "initialSubscriptions": { "type": "array", "items": { @@ -4605,6 +4630,10 @@ "type": "number", "description": "Current server sequence number" }, + "serverInfo": { + "$ref": "#/$defs/Implementation", + "description": "Optional identity of the server implementation (name and version).\nInformational only — see {@link Implementation} for how it may and may not\nbe used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`}\nidentifies the negotiated protocol, `serverInfo` identifies the host\nsoftware behind it." + }, "snapshots": { "type": "array", "items": { diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 90e0bc66..e7f8a2c5 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -1420,7 +1420,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItem const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, - { name: 'ClientCapabilities' }, + { name: 'ClientCapabilities' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 71c482df..fa42bdcb 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -1397,7 +1397,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItem const COMMAND_STRUCTS = [ 'InitializeParams', 'InitializeResult', - 'ClientCapabilities', + 'ClientCapabilities', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index 3b436969..d470f9c7 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -1369,7 +1369,7 @@ const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItem const COMMAND_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: string }[] = [ { name: 'InitializeParams' }, { name: 'InitializeResult' }, - { name: 'ClientCapabilities' }, + { name: 'ClientCapabilities' }, { name: 'Implementation' }, { name: 'ReconnectParams' }, { name: 'ReconnectReplayResult', omitDiscriminants: true }, { name: 'ReconnectSnapshotResult', omitDiscriminants: true }, diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 3d7792ee..81e7c174 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -1314,7 +1314,7 @@ function generateActionsFile(project: Project): string { const COMMAND_ENUMS = ['ReconnectResultType', 'ContentEncoding', 'CompletionItemKind', 'ResourceType', 'ResourceWriteMode']; const COMMAND_STRUCTS = [ - 'InitializeParams', 'InitializeResult', 'ClientCapabilities', + 'InitializeParams', 'InitializeResult', 'ClientCapabilities', 'Implementation', 'ReconnectParams', 'ReconnectReplayResult', 'ReconnectSnapshotResult', 'SubscribeParams', 'SubscribeView', 'SubscriptionDeliveryOptions', 'SubscribeResult', 'SessionForkSource', 'CreateSessionParams', 'DisposeSessionParams', diff --git a/types/common/commands.ts b/types/common/commands.ts index 7923c568..cbdb73e5 100644 --- a/types/common/commands.ts +++ b/types/common/commands.ts @@ -100,6 +100,36 @@ export interface PaginatedResult { // ─── initialize ────────────────────────────────────────────────────────────── +/** + * Identifies a protocol implementation — the software (and build) on one end + * of the connection, as distinct from the {@link AgentInfo | agent persona} it + * hosts. Carried as {@link InitializeParams.clientInfo | `clientInfo`} on the + * client side and {@link InitializeResult.serverInfo | `serverInfo`} on the + * server side, mirroring LSP's `clientInfo`/`serverInfo` and MCP's + * `Implementation`. + * + * This is **informational only**: it exists for logging, telemetry, an + * about/status affordance, and — as a last resort — a known-issue workaround + * for a specific buggy build. It is **not** a feature-detection mechanism. + * Feature availability stays with the capability model + * ({@link ClientCapabilities} and the various `*.capabilities` declarations); + * implementations SHOULD NOT gate protocol behaviour on parsing + * {@link Implementation.version | `version`}. + * + * @category Commands + */ +export interface Implementation { + /** Implementation name, e.g. a product or package identifier. */ + name: string; + /** + * Implementation version. A [SemVer](https://semver.org) string is + * recommended but not required. + */ + version?: string; + /** Optional human-readable display name. */ + title?: string; +} + /** * Establishes a new connection and negotiates the protocol version. * This MUST be the first message sent by the client. @@ -125,6 +155,14 @@ export interface InitializeParams extends BaseParams { protocolVersions: string[]; /** Unique client identifier */ clientId: string; + /** + * Optional identity of the client implementation (name and version). + * Informational only — see {@link Implementation} for how it may and may not + * be used. Distinct from {@link InitializeParams.clientId | `clientId`}, + * which is an opaque per-connection identifier used for reconnection, not a + * human-readable implementation name. + */ + clientInfo?: Implementation; /** URIs to subscribe to during handshake */ initialSubscriptions?: URI[]; /** @@ -187,6 +225,14 @@ export interface InitializeResult { protocolVersion: string; /** Current server sequence number */ serverSeq: number; + /** + * Optional identity of the server implementation (name and version). + * Informational only — see {@link Implementation} for how it may and may not + * be used. Whereas {@link InitializeResult.protocolVersion | `protocolVersion`} + * identifies the negotiated protocol, `serverInfo` identifies the host + * software behind it. + */ + serverInfo?: Implementation; /** Snapshots for each `initialSubscriptions` URI */ snapshots: Snapshot[]; /** Suggested default directory for remote filesystem browsing */ diff --git a/types/test-cases/round-trips/028-implementation-identity.json b/types/test-cases/round-trips/028-implementation-identity.json new file mode 100644 index 00000000..58364fa1 --- /dev/null +++ b/types/test-cases/round-trips/028-implementation-identity.json @@ -0,0 +1,18 @@ +{ + "name": "implementation-identity", + "group": "A", + "description": "The handshake `Implementation` identity (carried as InitializeParams.clientInfo / InitializeResult.serverInfo) decodes and re-encodes its required `name` plus optional `version` and `title` through the real generated type in every client.", + "type": "Implementation", + "input": { + "name": "acme-agent-host", + "version": "1.4.2", + "title": "ACME Agent Host" + }, + "acceptableOutputs": [ + { + "name": "acme-agent-host", + "version": "1.4.2", + "title": "ACME Agent Host" + } + ] +} From 4a38fcec9c71a9ab07e553f3b80733f9f999f73f Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Tue, 7 Jul 2026 06:25:06 -0500 Subject: [PATCH 2/3] Fix Rust ahp crate construction sites for new client_info field The hand-written ahp client crate constructed InitializeParams with a struct literal, and the ahp-types crate docs include an initialize doctest; both needed the new client_info field to compile after the types change. The client convenience method sets it to None (matching the existing locale/capabilities handling), and the doctest now shows a populated Implementation to demonstrate the field. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- clients/rust/crates/ahp-types/src/lib.rs | 7 ++++++- clients/rust/crates/ahp/src/client.rs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/clients/rust/crates/ahp-types/src/lib.rs b/clients/rust/crates/ahp-types/src/lib.rs index e16ddb03..8c60a9cd 100644 --- a/clients/rust/crates/ahp-types/src/lib.rs +++ b/clients/rust/crates/ahp-types/src/lib.rs @@ -57,7 +57,7 @@ //! ## Build an `initialize` request //! //! ``` -//! use ahp_types::commands::InitializeParams; +//! use ahp_types::commands::{Implementation, InitializeParams}; //! use ahp_types::messages::{JsonRpcMessage, JsonRpcRequest, JsonRpcVersion}; //! use ahp_types::common::AnyValue; //! @@ -68,6 +68,11 @@ //! initial_subscriptions: Some(vec!["ahp-root://".into()]), //! locale: Some("en".into()), //! capabilities: None, +//! client_info: Some(Implementation { +//! name: "my-host".into(), +//! version: Some("1.0.0".into()), +//! title: Some("My Host".into()), +//! }), //! }; //! //! let req = JsonRpcMessage::Request(JsonRpcRequest { diff --git a/clients/rust/crates/ahp/src/client.rs b/clients/rust/crates/ahp/src/client.rs index 5381eea8..f5d46022 100644 --- a/clients/rust/crates/ahp/src/client.rs +++ b/clients/rust/crates/ahp/src/client.rs @@ -366,6 +366,7 @@ impl Client { }, locale: None, capabilities: None, + client_info: None, }; self.request("initialize", params).await } From 183039c4a1241bdc891127e8f2fe336eade4e8e9 Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Tue, 7 Jul 2026 06:28:09 -0500 Subject: [PATCH 3/3] Include clientInfo/serverInfo in lifecycle handshake summary The quick-glance handshake signature at the top of the connection lifecycle doc omitted the new optional clientInfo/serverInfo fields, making it inconsistent with the JSON examples and the Implementation identity section below. Add them to the summary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/specification/lifecycle.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specification/lifecycle.md b/docs/specification/lifecycle.md index 5fc90338..37939d42 100644 --- a/docs/specification/lifecycle.md +++ b/docs/specification/lifecycle.md @@ -7,8 +7,8 @@ The connection lifecycle defines how an AHP client and server establish, resume, The client initiates the connection with an `initialize` **request**. The client offers a list of protocol versions it can speak; the server picks one and responds with the negotiated version and initial state snapshots: ``` -1. Client → Server: initialize(protocolVersions[], clientId, initialSubscriptions?, locale?) -2. Server → Client: { protocolVersion, serverSeq, snapshots[], defaultDirectory? } +1. Client → Server: initialize(protocolVersions[], clientId, clientInfo?, initialSubscriptions?, locale?) +2. Server → Client: { protocolVersion, serverSeq, serverInfo?, snapshots[], defaultDirectory? } ``` ### Initialize (Client → Server)