diff --git a/src/framework.ts b/src/framework.ts index 19187bb..baa3f48 100644 --- a/src/framework.ts +++ b/src/framework.ts @@ -8926,6 +8926,16 @@ export class AgentFramework { deniedCapabilities: string[]; /** Host-owned authority; deliberately separate from the portable grant. */ allowHostCommands: boolean; + /** + * Per-transport ยง17 facts about the last manifest this host fetched and + * acted on. The revision is server-authored and equality-only; these are + * not the server's manifestChanged announcements. + */ + manifestState: { + lastValidatedRevision: string | null; + lastFetchedAt: number | null; + lastNegotiatedAt: number | null; + }; command?: string; url?: string; }> { @@ -8934,6 +8944,11 @@ export class AgentFramework { toolPrefix: string; toolCount: number; policyEstablished: boolean; effectiveGrant: string[]; maskedCapabilities: string[]; deniedCapabilities: string[]; allowHostCommands: boolean; + manifestState: { + lastValidatedRevision: string | null; + lastFetchedAt: number | null; + lastNegotiatedAt: number | null; + }; command?: string; url?: string; }> = []; for (const [id, config] of this.mcplServerConfigs) { @@ -8951,6 +8966,9 @@ export class AgentFramework { maskedCapabilities: [...(connection?.droppedCapabilities ?? [])].sort(), deniedCapabilities: [...(connection?.grant.deniedPaths ?? [])].sort(), allowHostCommands: config.allowHostCommands === true, + manifestState: connection + ? { ...connection.manifestState } + : { lastValidatedRevision: null, lastFetchedAt: null, lastNegotiatedAt: null }, command: config.command, url: config.url, }); diff --git a/test/mcpl-list-status.test.ts b/test/mcpl-list-status.test.ts index 3bf10f5..5db04da 100644 --- a/test/mcpl-list-status.test.ts +++ b/test/mcpl-list-status.test.ts @@ -22,15 +22,22 @@ test('listMcplServers exposes the live grant layers and host-owned authority', ( new Set(['channels.incoming', 'channels.publish']), ['contextHooks.beforeInference.inject.system'], ); - const framework = frameworkWithConnection({ + const connection = { isConnected: true, willReconnect: true, policyEstablished: true, grant, droppedCapabilities: new Set(['channels.streaming']), - }); + manifestState: { + lastValidatedRevision: 'sha256:validated', + lastFetchedAt: 1_786_000_000_000, + lastNegotiatedAt: 1_786_000_000_100, + }, + }; + const framework = frameworkWithConnection(connection); - assert.deepEqual(framework.listMcplServers(), [{ + const listed = framework.listMcplServers(); + assert.deepEqual(listed, [{ id: 'discord', connected: true, retrying: false, @@ -41,9 +48,15 @@ test('listMcplServers exposes the live grant layers and host-owned authority', ( maskedCapabilities: ['channels.streaming'], deniedCapabilities: ['contextHooks.beforeInference.inject.system'], allowHostCommands: true, + manifestState: { + lastValidatedRevision: 'sha256:validated', + lastFetchedAt: 1_786_000_000_000, + lastNegotiatedAt: 1_786_000_000_100, + }, command: 'node', url: undefined, }]); + assert.notStrictEqual(listed[0]!.manifestState, connection.manifestState); }); test('listMcplServers distinguishes a disconnected retrying stub from connected', () => { @@ -53,6 +66,11 @@ test('listMcplServers distinguishes a disconnected retrying stub from connected' policyEstablished: false, grant: CapabilityGrant.empty(), droppedCapabilities: new Set(), + manifestState: { + lastValidatedRevision: null, + lastFetchedAt: null, + lastNegotiatedAt: null, + }, }); const [status] = framework.listMcplServers(); @@ -60,4 +78,9 @@ test('listMcplServers distinguishes a disconnected retrying stub from connected' assert.equal(status.retrying, true); assert.equal(status.policyEstablished, false); assert.deepEqual(status.effectiveGrant, []); + assert.deepEqual(status.manifestState, { + lastValidatedRevision: null, + lastFetchedAt: null, + lastNegotiatedAt: null, + }); });